- Volatile关键字—内存可见性问题
当多个线程访问共享数据的时候,多个线程之间是彼此不可见的,此时就会出现线程安全问题,但是我们知道使用synchronized关键字进行加锁实现同步是可以解决问题的,但是这种方法的效率比较低。
volatile的作用就是多个线程访问共享数据的时候,在内存中的共享数据对多个线程彼此是可见的。相较于synchronized是一种轻量级的同步策略。禁止指令重排。
注意:volatile不具备原子性(共享数据如果执行的是多个操作,那么volatile不能保证多个操作在多线程中是正确的),也不具备互斥性(synchronized的互斥性体现在某一时刻只能有一个线程访问共享数据)
- 原子变量-CAS算法
原子变量:juc.atomic包中提供了大量的常用原子变量。主要实现涉及到两部分:volatile保证共享数据的内存可见性,然后使用CAS(compare-And-Swap)算法来保证数据的原子性。CAS算法包含:内存值V,旧值A,更新值B:当且仅当V == A时,V = B;否则将不做任何操作。(CAS算法中涉及到三个数据,分别是读到内存中想要修改的值V,原来的旧值,想要更新的值,先把主存中的值读到内存中也就是内存值V,那么将要执行更新操作的时候再去判断此时的内存值和旧值时候一样,如果一样就把更新值B赋给V,然后重新写回主存;如果两者不相等,那么就什么操作都不做。)
- 关于锁的几个重要概念:
a) AQS -- 指AbstractQueuedSynchronizer类。 AQS是java中管理“锁”的抽象类,锁的许多公共方法都是在这个类中实现。AQS是独占锁(例如,ReentrantLock)和共享锁(例如,Semaphore)的公共父类。
b) AQS锁的类别 -- 分为“独占锁”和“共享锁”两种。
i. 独占锁 -- 在一个时间点只能被一个线程锁占有。根据锁的获取机制,它又划分为“公平锁”和“非公平锁”。公平锁,是按照通过CLH等待线程按照先来先得的规则,公平的获取锁;而非公平锁,则当线程要获取锁时,它会无视CLH等待队列而直接获取锁。独占锁的典型实例子是ReentrantLock,此外,ReentrantReadWriteLock.WriteLock也是独占锁。
ii. 共享锁--能被多个线程同时拥有,能被共享的锁。JUC包中的ReentrantReadWriteLock.ReadLock,CyclicBarrier,CountDownLatch和Semaphore都是共享锁。这些锁的用途和原理,在以后的章节再详细介绍。
c) CLH队列 -- Craig, Landin, and Hagersten lock queue
i. CLH队列是AQS中“等待锁”的线程队列。在独占锁中,竞争资源在一个时间点只能被一个线程锁访问;而其它线程则需要等待。CLH就是管理这些“等待锁”的线程的队列。
ii. CLH是一个非阻塞的 FIFO 队列。也就是说往里面插入或移除一个节点的时候,在并发条件下不会阻塞,而是通过自旋锁和 CAS 保证节点插入和移除的原子性。
- ConcurrentHashMap锁分段机制
java.util.concurrent 包中提供了多种并发容器类来改进同步容器的性能。ConcurrentHashMap同步容器类是Java 5 增加的一个线程安全的哈希表。HashTable是一种线程安全的Hash表,HashMap是非线程安全的Hash表,但是HashTable是一种独占锁,当多线程访问HashTable的时候,会把整个表全部锁起来,同一时间只能有一个线程访问,效率低不说,而且Hash table对符合操作是非线程安全的。
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock充当锁,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。
刚开始ConcurrentHashMap的实现是基于分段锁协议的实现,多个线程访问哈希表具有随机性,基于这种考虑来将数据存储在不同的哈希段上,然后每一个段配有一把锁,操作哪个段就在哪个段上加锁,此时,其他线程访问其他段是不阻塞的,但是对于该段的线程访问就需要等待,直到这个加锁的线程释放了锁,其他线程才能进行访问。
在java 8中,ConcurrentHashMap的实现继承了这种分散线程竞争压力的思想,java 8在实现ConcurrentHashMap的时候大量使用了CAS操作,减少了使用锁的频度来提高系统的响应度,锁和CAS在做并发在复杂度上不是一个数量级的,使用锁在很大程度上假设了多个线程的排斥性,并且使用锁会将线程阻塞等待,也就是说使用锁来做线程同步的时候,线程的状态是会改变的,但是使用CAS是不会改变线程的状态的(不太严谨的说),所以使用CAS比起使用synchronized或者使用Lcok来说更为轻量级。
- AbstractQueuedSynchronizer
就是被称之为AQS的类,它是一个非常有用的超类,可用来定义锁以及依赖于排队阻塞线程的其他同步器;ReentrantLock,ReentrantReadWriteLock,CountDownLatch,CyclicBarrier和Semaphore等这些类都是基于AQS类实现的。AbstractQueuedLongSynchronizer 类提供相同的功能但扩展了对同步状态的 64 位的支持。两者都扩展了类 AbstractOwnableSynchronizer(一个帮助记录当前保持独占同步的线程的简单类)。
- ReadWriteLock
接口以和Lock类似的方式定义了一些读取者可以共享而写入者独占的锁。JUC包只有一个类实现了该接口,即 ReentrantReadWriteLock
- ReentrantLock
ReentrantLock是独占锁。所谓独占锁,是指同一个时间点只能被一个线程锁获取到的锁。ReentrantLock锁包括"公平的ReentrantLock"和"非公平的ReentrantLock"。"公平的顾名思义,ReentrantLock锁在同一个时间点只能被一个线程锁持有;而可重入的意思是,ReentrantLock锁,可以被单个线程多次获取。
“锁”是为了保护竞争资源,防止多个线程同时操作线程而出错,ReentrantLock在同一个时间点只能被一个线程获取;ReentraantLock是通过一个FIFO的等待队列来管理获取该锁所有线程的。在“公平锁”的机制下,线程依次排队获取锁;而“非公平锁”在锁是可获取状态时,不管自己是不是在队列的开头都会获取锁。
- Condition控制线程通信
在synchronized中我们实现线程通信的方法是通过wait,notify,notifyAll实现的,但是我们如果使用 Lock来实现同步机制的话,就不能再用这几种通信方法,这就需要使用Condition控制线程之间的通信,这两者本质是一样的,只是不同的上锁方法需要通信的方法不一样。通过Lock的实现类对象调用newCondition方法来创建Condition对象,然后await、single、singleAll是和 wait,notify,notifyAll一一对应的。
- CountDownLatch闭锁
闭锁的作用就是在执行某些运算时候,当前线程等待其他线程全部执行完毕之后再执行当前线程。
举例来说:如何计算是个同时进行的线程的执行时间呢?如果一般的方法我们是很难计算出来的,但是闭锁可以完成这个功能。我们可以在主线程中创建一个闭锁CountDownLatch的对象,并且指定当前的闭锁需要管理的线程数量,然后把这个创建的闭锁对象作为参数传递给线程类,每当启动一个线程就要是管理对象的数量减1(有专门的方法调用执行即可),而且还要在当前的主线程中控制:分线程没有执行完毕主线程不能继续往下执行(也有专门的方法调用执行)。这样就能实现计算分线程执行时间的功能。
- CyclicBarrier
CyclicBarrier是一个同步辅助类,允许一组线程互相等待,全部到达某个公共屏障点 (common barrier point)之后在继续往下执行。因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier。
CyclicBarrier和CountDownLatch的区别是:
(01) CountDownLatch的作用是允许1或N个线程等待其他线程完成执行;而CyclicBarrier则是允许N个线程相互等待。
(02) CountDownLatch的计数器无法被重置;CyclicBarrier的计数器可以被重置后使用,因此它被称为是循环的barrier。
- Semaphore
Semaphore是一个计数信号量,它的本质是一个"共享锁"。
信号量维护了一个信号量许可集。线程可以通过调用acquire()来获取信号量的许可;当信号量中有可用的许可时,线程能获取该许可;否则线程必须等待,直到有可用的许可为止。 线程可以通过release()来释放它所持有的信号量许可。
- 线程池
a) 线程池架构图
b) workers是HashSet<Work>类型,即它是一个Worker集合。而一个Worker对应一个线程,也就是说线程池通过workers包含了"一个线程集合"。当Worker对应的线程池启动时,它会执行线程池中的任务;当执行完一个任务后,它会从线程池的阻塞队列中取出一个阻塞的任务来继续运行。wokers的作用是,线程池通过它实现了"允许多个线程同时运行"。
c) 提供了一个线程队列,队列中保存着处于等待状态的线程,只要给这些线程分配任务,这些线程就能够立即执行响应的功能。这样就避免的频繁创建和销毁线程的额外开销。
d) 线程池的任务管理策略:
i. 如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会分配一个线程去执行这个任务;
ii. 如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到阻塞队列(workQueue是BlockingQueue类型,即它是一个阻塞队列。当线程池中的线程数超过它的容量的时候,线程会进入阻塞队列进行阻塞等待。通过workQueue,线程池实现了阻塞功能。)当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理;
iii. 如果线程池中的线程数量大于 corePoolSize时,此时若某线程空闲时间超过keepAliveTime,该线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。
e) 线程池的拒绝策略
a) 线程池的拒绝策略,是指当任务添加到线程池中被拒绝,而采取的处理措施。
b) 当任务添加到线程池中之所以被拒绝,可能是由于:第一,线程池异常关闭。第二,任务数量超过线程池的最大限制。
c) 线程池共包括4种拒绝策略,它们分别是:AbortPolicy, CallerRunsPolicy, DiscardOldestPolicy和DiscardPolicy。
i. AbortPolicy -- 当任务添加到线程池中被拒绝时,它将抛出 RejectedExecutionException 异常。
ii. CallerRunsPolicy -- 当任务添加到线程池中被拒绝时,会在线程池当前正在运行的Thread线程池中处理被拒绝的任务。
iii. DiscardOldestPolicy -- 当任务添加到线程池中被拒绝时,线程池会放弃等待队列中最旧的未处理任务,然后将被拒绝的任务添加到等待队列中。
iv. DiscardPolicy -- 当任务添加到线程池中被拒绝时,线程池将丢弃被拒绝的任务。
- ForkJoinPool分支/合并框架
就是在必要的情况下,将一个大任务,进行拆分(fork)成若干个小任务(拆到不可再拆时),再将一个个的小任务运算的结果进行 join 汇总。
Fork/Join 框架与线程池的区别 :
采用 “工作窃取”模式(work-stealing): 相对于一般的线程池实现, fork/join框架的优势体现在对其中包含的任务的处理方式上.在一般的线程池中, 如果一个线程正在执行的任务由于某些原因无法继续运行,那么该线程会处于等待状态。 而在fork/join框架实现中,如果某个子问题由于等待另外一个子问题的完成而无法继续运行。 那么处理该子问题的线程会主动寻找其他尚未运行的子问题来执行.这种方式减少了线程的等待时间,提高了性能 。