## 27 倒数计时开始,三、二、一—CountDownLatch详解
> 时间像海绵里的水,只要你愿意挤,总还是有的。
> ——鲁迅
本节开始我们学习一些新的东西,不再局限于线程、锁、并发容器这些内容。JCU 包中提供了一些工具,用于线程间的协调。这些工具并不是每个并发编程的场景都需要使用。大部分场景通过 wait/nofity 或者 join 等操作就可以解决。但是在一些特定的场景下,我们则需要借助这些工具来解决问题。本节我们先来学习 CountDownLatch。
## 1、理解 CountDownLatch
从字面理解 CountDownLatch,意思是倒数门闩。它的作用是多个线程做汇聚。主线程开启了 A、B、C 三个线程做不同的事情,但是主线程需要等待 A、B、C 三个线程全部完成后才能继续后面的步骤。此时就需要 CountDownLatch 出马了。CountDownLatch 会阻塞主线程,直到计数走到 0,门闩才会打开,主线程继续执行。而计数递减是每个线程自己操作 CountDownLatch 对象实现的。如下图:
![图片描述](https://img.mukewang.com/5dd5fad80001e9c816000908.jpg)
这种场景在我们的生活中十分常见。比如篮球比赛中,作为控球后卫,如果没有快攻机会,那就需要等到中锋、大前锋、小前锋、得分后卫都跑到位了,我才能决定怎么组织进攻。又比如我们报团去旅游,必须所有人都到机场了,才能一起出发。
对于我们的程序来说这种场景也挺多的,比如你的订单信息可能需要从多个微服务取得数据,汇总后加工才返回给前台。此时从多个微服务取得数据可以是多个子线程来完成。
对于以上场景,都是 CountDownLatch 的用武之地。
## 2、如何使用 CountDownLatch
我们来模拟打篮球的例子,主线程假如是控球后卫,我们看一下如果不用 CountDownLatch 会有什么问题:
~~~java
public static void main(String[] args) throws InterruptedException {
System.out.println("控球后卫到位!等待所有位置球员到位!");
new Thread(()->{
System.out.println("得分后卫到位!");
}).start();
new Thread(()->{
System.out.println("中锋到位!");
}).start();
new Thread(()->{
System.out.println("大前锋到位!");
}).start();
new Thread(()->{
System.out.println("小前锋到位!");
}).start();
System.out.println("全部到位,开始进攻!");
}
~~~
输出为:
~~~
控球后卫到位!等待所有位置球员到位!
得分后卫到位!
中锋到位!
大前锋到位!
全部到位,开始进攻!
小前锋到位!
~~~
可以看到小前锋还没有到位,就开始进攻了。这显然和需求不符。出现这种结果也很好理解,因为代码中控球后卫并没有等每个球员的线程到位,就开始进攻了。
正确的姿势应该如下:
~~~java
public class Client {
private static final CountDownLatch countDownLatch = new CountDownLatch(5);
public static void main(String[] args) throws InterruptedException {
System.out.println("控球后卫到位!等待所有位置球员到位!");
countDownLatch.countDown();
new Thread(()->{
System.out.println("得分后卫到位!");
countDownLatch.countDown();
}).start();
new Thread(()->{
System.out.println("中锋到位!");
countDownLatch.countDown();
}).start();
new Thread(()->{
System.out.println("大前锋到位!");
countDownLatch.countDown();
}).start();
new Thread(()->{
System.out.println("小前锋到位!");
countDownLatch.countDown();
}).start();
countDownLatch.await();
System.out.print("全部到位,开始进攻!");
}
}
~~~
首先声明声明了一个 countDownLatch 对象,由于有5名球员,所以传入 count=5。每个球员的线程在球员到位后,都会执行 countDownLatch.countDown(),这个方法可以理解为我们把初始值的计数数量5做递减。当减到零时才会执行 countDownLatch.await(); 后面的代码。countDownLatch.await() 就是我们的门闩,这行代码做的是锁门操作,而每次 countDown(),调用5次后,门闩打开,后面的代码才被执行。
这段代码输出如下:
~~~
控球后卫到位!等待所有位置球员到位!
得分后卫到位!
中锋到位!
大前锋到位!
小前锋到位!
全部到位,开始进攻!
~~~
可以看出完全符合我们的预期,如果你还对此表示怀疑,那么你可以在某个线程中让其 sleep 上几秒,再看看是否还是全部到位才开始进攻。
## 3、CountDownLatch 的原理解析
CountDownLatch 内部其实还是借助 AQS 实现的。它内部实现了 AbstractQueuedSynchronizer。使用 AQS 的 state 变量来存储计数器的值,初始化 CountDownLatch,实际在初始化 state 值。
### 3.1 构造函数
我们看其构造函数:
~~~java
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
~~~
~~~java
Sync(int count) {
setState(count);
}
~~~
~~~java
protected final void setState(int newState) {
state = newState;
}
~~~
三个方法串起来看,发现最后就是把传入的 count 设置给了 state。
### 3.2 await 方法
await 方法会阻塞当前线程,代码如下:
~~~java
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
~~~
调用了 AQS 的方法:
~~~java
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
~~~
尝试获取共享锁 tryAcquireShared,如果不能获取进入等待队列。
tryAcquireShared 方法由 CountDownLatch 的内部类 Sync 实现,如下:
~~~java
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
~~~
可以看到如果 state 为0就直接返回了,但如果不为零,才进入等待队列。调用 tryAcquireShared 仅仅检查 state值,而不会对其减 1,可以看到传入的参数 acquires根本没有用。
我们再看看 countDown 方法。
### 3.3 countDown 方法
这个方法会对 state 递减。当计数器减为 0 时,所有阻塞的线程都被唤醒。代码如下:
~~~java
public void countDown() {
sync.releaseShared(1);
}
~~~
可见其也是通过对自己的 AQS 子类调用 releaseShared 方法:
~~~java
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
~~~
而在这个方法里,tryReleaseShared 是由子类实现的,也就是 countDown 中的 Sync 类,实现代码如下:
~~~java
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
~~~
以上代码在自旋中,通过 CAS 的方式对 state 值-1,如果 c-1 后等于 0,说明计数到 0。那么 releaseShared 中会调用 doReleaseShared(),让 AQS 释放资源出来。
以上对做 CountDownLatch 源代码做了简单的分析,可以看出主要是使用 AQS 来实现。通过阻塞队列阻塞线程。然后通过 state 值的初始化和递减,实现 state 为 0 时,激活阻塞的线程。
## 4、总结
CountDownLatch 有其一定的应用场景,对于多线程协调和串起流程有很大的帮助。我们在多线程开发中,可以留意是否有类似的场景,能够通过 CountDownLatch 来解决。CountDownLatch 自身也有一定的局限性,它只能被使用一次,而不能被恢复再次使用。下一节我们讲学习 CyclicBarrier,它可以重置以重复使用。
- 前言
- 第1章 Java并发简介
- 01 开篇词:多线程为什么是你必需要掌握的知识
- 02 绝对不仅仅是为了面试—我们为什么需要学习多线程
- 03 多线程开发如此简单—Java中如何编写多线程程序
- 04 人多力量未必大—并发可能会遇到的问题
- 第2章 Java中如何编写多线程
- 05 看若兄弟,实如父子—Thread和Runnable详解
- 06 线程什么时候开始真正执行?—线程的状态详解
- 07 深入Thread类—线程API精讲
- 08 集体协作,什么最重要?沟通!—线程的等待和通知
- 09 使用多线程实现分工、解耦、缓冲—生产者、消费者实战
- 第3章 并发的问题和原因详解
- 10 有福同享,有难同当—原子性
- 11 眼见不实—可见性
- 12 什么?还有这种操作!—有序性
- 13 问题的根源—Java内存模型简介
- 14 僵持不下—死锁详解
- 第4章 如何解决并发问题
- 15 原子性轻量级实现—深入理解Atomic与CAS
- 16 让你眼见为实—volatile详解
- 17 资源有限,请排队等候—Synchronized使用、原理及缺陷
- 18 线程作用域内共享变量—深入解析ThreadLocal
- 第5章 线程池
- 19 自己动手丰衣足食—简单线程池实现
- 20 其实不用造轮子—Executor框架详解
- 第6章 主要并发工具类
- 21 更高级的锁—深入解析Lock
- 22 到底哪把锁更适合你?—synchronized与ReentrantLock对比
- 23 按需上锁—ReadWriteLock详解
- 24 经典并发容器,多线程面试必备—深入解析ConcurrentHashMap上
- 25 经典并发容器,多线程面试必备—深入解析ConcurrentHashMap下
- 26不让我进门,我就在门口一直等!—BlockingQueue和ArrayBlockingQueue
- 27 倒数计时开始,三、二、一—CountDownLatch详解
- 28 人齐了,一起行动—CyclicBarrier详解
- 29 一手交钱,一手交货—Exchanger详解
- 30 限量供应,不好意思您来晚了—Semaphore详解
- 第7章 高级并发工具类及并发设计模式
- 31 凭票取餐—Future模式详解
- 32 请按到场顺序发言—Completion Service详解
- 33 分阶段执行你的任务-学习使用Phaser运行多阶段任务
- 34 谁都不能偷懒-通过 CompletableFuture 组装你的异步计算单元
- 35拆分你的任务—学习使用Fork/Join框架
- 36 为多线程们安排一位经理—Master/Slave模式详解
- 第8章 总结
- 37 结束语