这是一篇学习总结,不会深入到细节,不过好处是:可以快速获得对java并发编程的整体认识,并且会了解到一些核心的原理。
1 线程池
2 Future
3 锁:ReentrantLock、AbstractQueuedSynchronizer和LockSupport工具类
4 队列(阻塞、非阻塞,优先队列、延迟队列)
5 并发容器类:ConcurrentHashMap/CopyOnWriteArrayList/ConcurrentSkipListSet
6 并发原子类
7 属性的Updater工具类:AtomicIntegerFieldUpdater/AtomicLongFieldUpdater/AtomicReferenceFieldUpdater
8 ThreadLocal
9 同步器:Semaphore、CountDownLatch、CyclicBarrier、Phaser
线程池主要有两个:ThreadPoolExecutor和ScheduledThreadPoolExecutor。
1 ThreadPoolExecutor
可定制性比较高,包括核心线程数、最大线程数、线程keep-alive时间、ThreadFactory、任务队列、拒绝策略等。
里面这么多属性里最主要的两个属性,非任务队列workQuue和线程池workers莫属。
当通过调用execute方法,将任务提交给线程池时,主要执行流程是:
(1)如果线程数还未达到核心线程数,则创建新的核心线程,并直接将任务交给新线程执行;
(2)如果已经达到核心线程数,则将任务加入队列;
(3)如果队列已满,则创建非核心线程,直到达到最大线程数。
(3)如果这时还有任务来,则执行拒绝策略,默认的策略是抛出异常,另一个可选的实用策略是让调用方线程来执行任务。
核心线程数默认有任务提交过来时才会创建,但是可以通过调用prestartAllCoreThreads()来提前创建所有核心线程。
2 ScheduledThreadPoolExecutor
继承了ThreadPoolExecutor,它可以调度延迟任务和周期任务。
定时任务调度,要解决两个问题:
(1)任务到点后,如何触发执行。
(2)周期任务执行后,如何继续调度。
ScheduledThreadPoolExecutor依靠两个对象DelayedWorkQueue和ScheduledFutureTask,再加上一些小技巧,解决了这两个问题。
第一个问题的关键是延迟队列。DelayedWorkQueue是个堆数据结构,元素按延迟时间大小组成小根堆,最快到期的任务放在堆顶。只有到点执行的任务才会从DelayedWorkQueue弹出,当线程从DelayedWorkQueue队列获取元素时,先检查堆顶任务是否到期,无到期则做time-wait,time-wait的时间长度是堆顶任务剩余的到点时间。这样当线程从time-wait恢复时,刚好可以执行堆顶任务。堆顶任务出队列后,再组成堆,再检查堆顶。
ScheduledFutureTask继承自FutureTask,它的run方法的特点是,如果是周期任务,会将任务重新加入任务队列DelayedWorkQueue,从而等待下次调度。因而解决了第(2)个问题。
最典型的实现是FutureTask。
主要的属性是
Callable callable --真正需要执行的任务
Object outcome --任务的执行结果
WaitNode waiters --Future未完成时调用get方法获取任务结果而阻塞的线程队列。
其经典的扩展是任务调度线程池内部的ScheduledFutureTask,如上文所述。
4.1、综述
1 ReentrantLock和同步器比如CountDownLatch,与Synchronized关键字在数据结构上的明细区别是,Lock类只有一个set用于记录等待中的线程。一个线程尝试获取一个ReentrantLock类对象的锁而获取不到时,这个线程会被记录在lock的entry set,而当线程需要在获取锁后放弃然后等待,则需要在锁的condition上调用await,进入condition的wait set。这与Synchronized关键字有很大的不同。
2 而ReentrantLock和LockSupport工具类的重要区别是,LockSupport只是提供了将线程挂起的API(park/unpark),它是不会记录哪些线程被挂起的,需要调用方自己记录。比如FutureTask,它是没有使用Lock而使用LockSupport,内部有waiters链表,记录等待的线程。
需要根据具体需要决定使用ReentrantLock还是LockSupport。如果涉及到多线程共享数据,但是除了某些线程处理工作外,其他线程只是需要阻塞等待结果,即只需要阻塞的效果,并不需要获取锁进入临界区处理共享数据,那么很适合用LockSupport。
需要关注的是,ReentrantLock内部也需要LockSupport的支持,来完成锁定效果。
4.2、AbstractQueuedSynchronizer
1 有一个内部类Node,代表双向链表的节点,Node有个volatile Thread thread,代表入队列的线程。
2 通过Node定义了一个双向链表,表示等待锁的线程队列。exclusiveOwnerThread代表持有锁的线程。
3 通过Condition来分离调用await而进入等待的线程。这有个关键设计,ConditionObject实现为内部类,而不是静态内部类,利用内部类,可以方便地表示同步器和Condition之间的拥有关系。比如通过判断condition是否由指定同步器创建,即可判断归属关系:
public final boolean owns(ConditionObject condition) {
return condition.isOwnedBy(this);
}
final boolean isOwnedBy(AbstractQueuedSynchronizer sync) {
return sync == AbstractQueuedSynchronizer.this;
}
4.3、ReentrantLock
依赖同步器来实现,支持多条件分离等待线程,支持公平锁和非公平锁。
使用公平锁时,自己必须是entry set链表里面的第一个,或者entry set为空时,才会成功获取锁。
4.4、LockSupport工具类
主要是通过park/unpark挂起和恢复线程,将线程挂起时,会设置线程的parkBlocker属性,以记录线程是被哪个对象挂起的。
2.1 阻塞队列
主要是ArrayBlockingQueue/LinkedBlockingQueue.
1 ArrayBlockingQueue内部用一个数组存储元素,读写都需要加锁。
特色是这个数组是环状的,通过takeIndex和putIndex分别进行读和写,两个index在增长到数组大小时归零,从而实现环状数组的效果。通过count控制,避免写入的新元素覆盖已有数据。通过这两个index,也可以避免了元素出队列时,后面元素需要往前移动的缺点。
通过notEmpty和notFull两个不同的Condition来分离读线程和写线程的阻塞和唤醒。
2 LinkedBlockingQueue是基于链表,如果不指定大小,那么队列最大容量为Integer.MAX_VALUE,即2^31-1。
特色是通过putLock和takeLock两个锁分离入队列和出队列的同步操作:入队列时,锁住的是last节点;出队列时,锁住的是head节点。
3 SynchronousQueue,它主要用于将任务从一个线程传给另外一个线程。内部通过Transferer实现,有两种Transferer,当fair==true,使用TransferQueue,否则使用TransferStack。
这个实现比较有意思,以TransferQueue为例,当没有consumer消费任务时,所有producer线程都阻塞等到,用一个内部用一个链表表示。当有消费者来到时,一次处理链表各个任务节点并唤醒生产者线程。它的队列里,要么都是生产者线程节点,要么都是消费者线程节点,要么队列是空的。
2.2 非阻塞队列
最典型的是ConcurrentLinkedQueue,其本身的数据结构比较简单,就是一个单链表,用head指向表头节点,tail指向最后一个节点(它们都指向真正的元素,都不是dummy node;当然无元素时,指向一个空节点,即dummy node)。
最大的特色是,入队列和出队列,都不使用互斥锁,而是CAS操作。不过CAS操作在这里的使用场景稍微复杂,需要考虑仔细。
比如当节点入队列时,由于可能有多线程并发进行入队列操作,所以tail指针可能是不断在变的,那么如果CAS失败,需要重新往后找到队尾节点,重新尝试CAS操作,直到成功。每次节点入队列,通常都会涉及到两个CAS操作,一是通过CAS操作将入队列元素放到队尾,二是通过CAS操作将新入队列的元素设置为新的队尾元素。
出队列也会有类似情况。它的两个CAS操作,一是通过CAS将出队列的节点的值设置为null,二是将head往后移动。
head可能并不是每时每刻都指向队头,tail也不是每时每刻都指向队尾。
它们有一些不变性,保持了并发队列的正确操作,具体直接参考源码注释和源代码,写得很清楚。
2.3 优先队列和延迟队列
1 优先队列
有非线程安全版本PriorityQueue,和线程安全版本的PriorityBlockingQueue。
它们都是基于二叉堆数据结构实现的最小堆,每次队头元素出队列,都是从堆顶出,然后将最后一个元素移动到堆顶,再做旋转和变换,以保持堆的性质。
堆的操作主要有:siftDown/siftUp/初始化时构建堆。
2 延迟队列DelayQueue
可以看做是特殊的优先队列,其内部就有个优先队列类型的属性用于存储元素。延迟队列使用时间作为优先级,利用优先队列实现了延迟任务按时间排序效果,延迟时间最早到点的元素位于队头。
由于队列是无界的,因此入队列的操作不会阻塞。
出队列时,先检测队头元素h,但h不出队列。如果h延迟时间已经到点了,就将h出队列;如果h还没到点,看调用的方法,如果是poll()就直接返回null,如果是take则做time-wait或者wait,time-wait时是等待队头的剩余的延迟时间,这样一来,当队头元素到点时,线程刚好唤醒,从而处理元素。
DelayQueue内部有个优化,即只让一个线程针对队头元素做time-wait,其他线程都wait,以此来避免不必要的time-wait。因为大家如果都time-wait,到点一起唤醒时,其实可以出队列的就只有对头元素h一个而已,知会有一个线程拿到,其他线程接下来又继续等待。做time-wait的线程,叫leader线程。
当leader线程从等待中恢复时,要发出信号通知其他线程,让别的线程做time-wait并成为新的leader。因为如果不这么做,当队列中有元素到点,需要处理时,将没有线程来处理。
6.1 ConcurrentHashMap
1 主要属性
有两个数组table和nextTable,table用于提供服务,nextTable用于rehash。并且创建ConcurrentHashMap对象时,不会马上新建table数组,而是第一次往里面放数据时,才会初始化。
long baseCount用于记录Map中的元素个数。
int sizeCtl不同场景有有不同的值,代表不同的含义,当初始化后,其值代表Map需要进行rehash的阈值。
int transferIndex是用于rehash时,区隔哪些slots已经rehash,哪些slots还没迁移。
2 put/get
get不需要加锁,和普通的HashMap的get几乎相同。
put需要在对应的Hash Bucket加锁,然后再通过CAS放到冲突链上。为了节约空间,这里用Bucket的第一个节点作为锁对象,直接用Synchronized关键字上锁。这样不锁整个table而只锁单个Bucket的锁分离技术,极大地提高了并发性能。
同时,put还需要检查是否正在rehash,如果正在进行rehash,那么将放到新的数组上。
同时冲突链表到一定长度,会转换为红黑树。进行删除时,红黑树节点少于指定数量时,又会转换为链表结构。
普通的HashMap,在插入Bucket时,新元素都插入在头部,而ConcurrentHashMap是插入到尾部,这是为了用头部的节点作为锁对象。
3 rehash
支持多个线程都进行rehash。
6.2 CopyOnWriteArrayList
这个列表,虽然是线程安全的,但是也是通过加锁实现的。
其采用copy-on-write的方式,对所有写操作,加锁,然后将原数组copy一份,然后写。
读操作是不加锁的,读都在原数组上进行,所以适用于读多写少的并发访问场景,如果是写比较多,就不适用了,内存浪费严重。
CopyOnWriteArrayList的特性,特别适用于做本地缓存。
6.3 ConcurrentSkipListSet
跳表数据结构,可以参考redis的跳表。
7.1 AtomicInteger
内部有个volatile int value属性,操作这个属性,都是CAS的方式,根据属性的偏移量offset来比较并更新。提供的API方法常用的有getAndIncrement/incrementAndGet/compareAndSet等。这些原子类内部都会持有Unsafe的单例对象。
7.2 AtomicIntegerArray
可以原子性地更新每个数组的元素。和AtomicInteger的区别是,它里面记录的是数组的偏移量,记为base,当需要CAS每个数组元素时再根据元素的index和基本偏移量base,得到数组元素的偏移量,然后再CAS。
7.3 LongAdder/LongAccumulator
LongAdder
LongAdder是JDK1.8开始出现的,所提供的API基本上可以替换掉原先的AtomicLong。LongAdder所使用的思想就是热点分离,这一点可以类比一下ConcurrentHashMap的设计思想。就是将value值分离成一个数组,当多线程访问时,通过hash算法映射到其中的一个数字进行计数。而最终的结果,就是这些数组的求和累加。这样一来,就减小了锁的粒度。如下图所示:
在实现的代码中,LongAdder一开始并不会直接使用Cell[]存储。而是先使用一个long类型的base存储,当casBase()出现失败时,则会创建Cell[]。此时,如果在单个Cell上面出现了cell更新冲突,那么会尝试创建新的Cell,或者将Cell[]扩容为2倍。
LongAccumulator
LongAccumulator和LongAdder的原理是一样的,只是前者支持用户自定义聚合操作,比如不只是累加,还可以不断减少、或者相乘等。
AtomicIntegerFieldUpdater/AtomicLongFieldUpdater/AtomicReferenceFieldUpdater也是基于CAS,没啥好说的
相对于java的ThreadLocal实现,netty的FastThreadLocal很有特色。
各个同步器之间的区别,最好从线程间的协作方式方面来加以区分。
10.1 信号量
Semaphore类,也是基于AQS实现,信号量支持公平和非公平的方式。
基本原理是:有一个许可证池(其实内部实现只是个计数器),每当线程需要进入临界区时,先需要获取许可permit,如果池中还有可用的许可,则能获取到;获取到则继续运行,获取不到就阻塞等待。当别的线程退出临界区时,需要显示调用release方法归还permit,并signal其他线程以让它们恢复执行,恢复的线程重新尝试获取permit。
10.2 CountDownLatch
原理比较简单,基于AQS实现,思想是:创建时设置一个数值count,在多线程环境下,由某一个线程调用latch.await方法,进入阻塞等待的状态;其他线程执行完任务后调用latch.countDown方法将count减1,每次减1都对count进行检查,如果count下降到0,则将wait状态的线程恢复。
可以有多个线程调用latch.wait方法,count变为0时,它们都会被恢复。适用的场景是:某些线程需要等待其他线程完成任务时,才能开始执行自己的任务。
10.3 CyclicBarrier
CyclicBarrier适用的场景是,所有线程都完成任务了,都到一个同步点了,再开始执行同步点后面的任务。主要使用其await方法,原理是:创建barrier时,指定数值int parties,每个线程执行到需要同步的点时,调用barrier.await()方法等待其他线程,await方法每次都会检查parties是否到0,到0了就唤醒其他线程,同时当前调用await的线程也一起往后执行。
CyclicBarrier与CountDownLatch的区别是,CountDownLatch的场景里线程分两组,一组等待另外一组。而CyclicBarrier中,所有线程在一组里。
10.4 Phaser
和CyclicBarrier类似,但是更灵活。CyclicBarrier需要先设定parties数目,但是Phaser是不需要的,Phaser通过注册的方式,动态确定。使用Phaser时,先调用register()方法,注册party;然后当线程达到同步点时需要调用arriveAndAwaitAdvance(),声明自己已到达同步点,并等待其他线程达到。Phaser往往在caller线程或说主线程会先将caller线程自己添加为一个party,然后在caller线程调用arriveAndDeregister方法,让其他已经到同步点的线程往前执行。
1、线程与线程安全
参考《深入理解java虚拟机》
线程是用于提高程序并发性能的,有很多成熟的线程模型,比如netty的reactor,常见的生产者/消费者模型。
使用多线程操作共享资源时,就会涉及到线程安全问题,实现线程安全的主要手段有:互斥同步/非阻塞同步/无同步等方法。
如果使用互斥同步的方式,那么锁也有很多优化的手段,比如锁粗化、锁细化、锁消除、锁分离,以及自旋锁和自适应自旋、偏向锁、轻量级锁等。
2、synchronized锁原理和JVM的优化
参考《深入理解java虚拟机》
3、Fork/Join框架的原理和ForkJoinPool
多线程,执行的整个过程,和归并排序算法很像:先将任务不断地拆分到各个线程进行处理,然后再将每个子任务的结果合并。