CountDownLatch 是 Java 并发包(java.util.concurrent)中一个非常经典且实用的同步工具类,由 Doug Lea 设计。它的核心思想是:
让一个或多个线程等待,直到其他线程完成一组操作("倒计时归零")后再继续执行。
下面从设计意图、内部实现、使用场景、内存语义四个维度帮你彻底理解它。
一、设计意图:解决什么问题?
场景1:主线程等所有子任务完成
- 启动 N 个 worker 线程去处理任务。
- 主线程不能继续,必须等这 N 个都做完。
- ✅
CountDownLatch初始化为 N,每个 worker 完成后调用countDown(),主线程调用await()等待。
场景2:所有线程同时启动("发令枪")
- 所有 worker 线程先启动,但卡在
await()。 - 主线程准备就绪后调用
countDown()(初始 count=1),所有 worker 同时开始。 - ✅ 这就是"门闩"(latch)或"栅栏"的作用。
🔑 关键特性:
- 一次性(one-shot) :计数器只能从 N 减到 0,不能重置。
- 如果你需要重复使用(比如循环等待),请用
CyclicBarrier。
二、内部实现:基于 AQS 的共享模式
CountDownLatch 的核心是内部类 Sync,它继承自 AbstractQueuedSynchronizer(AQS)。
1. 状态表示
java
setState(count); // AQS 的 state 字段 = 倒计数值
state > 0:还有任务未完成,调用await()的线程要阻塞。state == 0:所有任务完成,所有等待线程被释放。
2. 获取(await)------ 共享模式
java
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
- 返回 1:表示获取成功(可以通行)。
- 返回 -1:表示获取失败(需要排队等待)。
- 注意:只要
state == 0,所有调用await()的线程都能立即通过 → 这就是共享模式的体现!
3. 释放(countDown)------ 共享释放
java
protected boolean tryReleaseShared(int releases) {
for (;;) {
int c = getState();
if (c == 0) return false; // 已经归零,无需再释放
int nextc = c - 1;
if (compareAndSetState(c, nextc))
return nextc == 0; // 只有归零时才触发唤醒
}
}
- 使用 CAS 循环安全地减 1。
- 只有当
nextc == 0时,才返回true,通知 AQS:"现在可以唤醒所有等待者了!"
4. 唤醒机制
- 当
countDown()使计数归零时,AQS 会调用doReleaseShared()(你之前问过的那个方法)。 - 因为是共享模式,所有等待线程都会被连续唤醒(传播机制确保不漏掉)。
✅ 所以:
CountDownLatch是 AQS 共享模式的经典应用。
三、典型使用方式(结合你的代码注释)
示例1:发令枪 + 完成信号
java
CountDownLatch startSignal = new CountDownLatch(1); // 所有工人等"开始"信号
CountDownLatch doneSignal = new CountDownLatch(N); // 主线程等"完成"信号
// 启动 N 个工人
for (int i = 0; i < N; i++)
new Thread(() -> {
startSignal.await(); // 阻塞,直到 startSignal.countDown()
doWork();
doneSignal.countDown(); // 完成一个,计数减1
}).start();
doSomethingElse(); // 主线程做准备工作
startSignal.countDown(); // 发令:所有工人开始!
doneSignal.await(); // 等待所有工人完成
示例2:并行任务汇总
java
ExecutorService exec = Executors.newFixedThreadPool(4);
CountDownLatch latch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
exec.submit(() -> {
try {
process(i);
} finally {
latch.countDown(); // 必须保证执行!
}
});
}
latch.await(); // 等待全部10个任务完成
exec.shutdown();
⚠️ 注意:
countDown()通常放在finally块中,防止异常导致计数不减,造成死锁。
四、内存可见性(Memory Consistency)
Java 内存模型(JMM)保证:
在某个线程调用
countDown()之前的所有操作,对在await()成功返回后的线程是可见的。
即:happens-before 关系成立。
这意味着:
- Worker 线程写入的数据(如计算结果到共享变量),
- 在主线程
await()返回后,一定能读到最新值。
✅ 无需额外加 volatile 或 synchronized!
五、与 CyclicBarrier 的区别
| 特性 | CountDownLatch |
CyclicBarrier |
|---|---|---|
| 是否可重用 | ❌ 一次性 | ✅ 可重复使用 |
| 触发条件 | 计数归零 | 所有线程到达屏障点 |
| 谁等待 | 任意线程(通常是协调者) | 所有参与线程互相等待 |
| 典型用途 | 等待 N 个任务完成 / 发令枪 | 多线程迭代同步(如并行计算每轮同步) |
六、总结:一句话理解 CountDownLatch
它是一个"倒计时门闩":门关着(count > 0),所有想通过的线程(调用 await)都得等着;每有人完成一件事(countDown),计数减一;当计数归零,门打开,所有等待者一拥而过,且之后来的人直接通行。
它的强大之处在于:
- 简单、高效、线程安全;
- 利用 AQS 共享模式实现"一对多"或"多对一"的同步;
- 提供明确的内存可见性保证。
这也是为什么它成为并发编程中的"瑞士军刀"之一。