CountDownLatch从字面上理解就是倒计时门栓的意思,它的实现原理同ReentrantLock一样,依然是借助AQS的双端队列,来实现原子的计数-1,线程阻塞和唤醒。
简介
CountDownLatch创建时设置一个count值,表示倒计时的次数,然后等待状态的线程调用CountDownLatch的await()方法(注意不要和Object.wait()混淆)进行等待,倒计时的方法是countDown(), 每次countDown都会减少count的值,直到count为0,则所有的await()的线程都会从等待中返回。
Demo
依然以讲解 ReentrantLock中的例子来说明,多线程实现累加:
1 | 线程1实现 10加到100 |
CountDownLatch的实现:
1 | public class CountDownLatchDemo { |
输出
1 | 线程3 : 开始执行 |
上面的流程:
- 首先是创建实例 CountDownLatch countDown = new CountDownLatch(2)
- 需要同步的线程执行完之后,计数-1; countDown.countDown()
- 需要等待其他线程执行完毕之后,再运行的线程,调用 countDown.await()实现阻塞同步
使用场景
一种是同时开始,另一种是主从协作。它们都有两类线程,互相需要同步。
在同时开始场景中,运动员线程等待主裁判线程发出开始指令信号,一旦发出后,所有运动员线程同时开始,计数初始为1,运动员调用await,主线程调用countDown。
在主从协作模式中,主线程依赖工作线程的结果,需要等待工作线程结束,这时,计数初始值为工作线程的个数,工作线程结束后调用countDown,主线程调用await进行等待。
实现原理
CountDownLatch借助AQS的双端队列,来实现原子的计数-1,线程阻塞和唤醒。
AQS
AQS使用一个FIFO的队列表示排队等待锁的线程,队列头节点称作“哨兵节点”或者“哑节点”,它不与任何线程关联。其他的节点与等待线程关联,每个节点维护一个等待状态waitStatus
1 | private transient volatile Node head; |
计数器的初始化
CountDownLatch内部实现了AQS,并覆盖了tryAcquireShared()和tryReleaseShared()两个方法,下面说明干嘛用的
通过前面的使用,清楚了计数器的构造必须指定计数值,这个直接初始化了 AQS内部的state变量
1 | Sync(int count) { |
后续的计数-1/判断是否可用都是基于sate进行的
countDown() 计数-1的实现
1 | // 计数-1 |
上面截出计数减1的完整调用链
- 尝试释放锁tryReleaseShared,实现计数-1
- 若计数已经小于0,则直接返回false
- 否则执行计数(AQS的state)减一
- 若减完之后,state==0,表示没有线程占用锁,即释放成功,然后就需要唤醒被阻塞的线程了
- 释放并唤醒阻塞线程 doReleaseShared
- 如果队列为空,即表示没有线程被阻塞(也就是说没有线程调用了 CountDownLatch#wait()方法),直接退出
- 头结点如果为SIGNAL, 则依次唤醒头结点下个节点上关联的线程,并出队
CountDownLatch计数为0之后,所有被阻塞的线程都会被唤醒,且彼此相对独立,不会出现独占锁阻塞的问题
await() 阻塞等待计数为0
1 | public void await() throws InterruptedException { |
阻塞的逻辑:
- 判断state计数是否为0,不是,则直接放过执行后面的代码
- 大于0,则表示需要阻塞等待计数为0
- 当前线程封装Node对象,进入阻塞队列
- 然后就是循环尝试获取锁,直到成功(即state为0)后出队,继续执行线程后续代码