CountDownLatch:一次性的共享门闩
🤔 道格·李为什么需要一个倒计时门闩
多线程编程里有一个反复出现的问题:主线程需要等待若干个子任务全部完成,然后汇总结果继续执行。在 JUC 出现之前,Java 只有两种方式应对:
Thread.join()——但 join() 等的是线程终止,不是任务完成。如果线程来自线程池(被复用,不会终止),join() 完全不适用。它是"等人死了"而不是"等人把活干完"。
自己写自旋等待——用一个 volatile 计数器,主线程循环检查。但自旋空转浪费 CPU,加 sleep 又会引入延迟,而且 count++ 本身不是原子操作。
道格·李在设计 JSR 166 时看到了这个空缺:需要一个轻量级的同步辅助工具,让一个(或多个)线程能够等待其他线程完成一组操作,不依赖线程终止,不浪费 CPU,而且足够简单。
这就是 CountDownLatch 的诞生背景。它用 AQS 的共享模式实现了一个一次性的倒计时器:计数器从 N 开始,每个子任务完成时减 1(countDown()),主线程在 await() 上阻塞直到计数器归零。
道格·李给它设计了几个刻意的约束:
- 一次性——计数器归零后无法重置。这个约束简化了实现(不需要考虑"重置时正在等待的线程怎么办"),也迫使使用者为可重复场景选用 CyclicBarrier
- 只减不增——
countDown()只能减少计数,无法增加。这也简化了状态机——计数器状态只有 N→0 这一个方向 - 基于 AQS 共享模式——让多个等待线程可以同时被唤醒(当计数器归零时),而不是排他锁那样只唤醒一个
设计理念 1:一次性的约束
CountDownLatch 最核心的设计约束是 一次性 ——一旦计数器从 N 减到 0,门闩永久打开,无法再关闭。
这个约束绝非"能力不足",而是 刻意的设计取舍 :
| 如果支持重置 | 代价 |
|---|---|
| 需要处理"已有线程在 await 上等待"和"重置后的新等待者"两种状态 | 状态机复杂度翻倍 |
countDown 和 reset 并发时的语义难以定义 | 需要额外的同步机制 |
| 每个等待者都要知道自己是"老批次"还是"新批次" | 需要代际(generation)标记 |
一次性约束消除了一个巨大的设计空间:时间维度上的状态管理。 CountDownLatch 只有两个有意义的状态: state > 0 (门闩关闭)和 state == 0 (门闩打开)。从关闭到打开只需要 state 单向递减,不需要考虑"打开了又关上"的复杂路径。
stateDiagram-v2
state "state > 0\n门闩关闭\nawait() 阻塞" as CLOSED
state "state == 0\n门闩打开\nawait() 立即返回" as OPEN
[*] --> CLOSED : new CountDownLatch(n)
CLOSED --> CLOSED : countDown()\nstate-- but still > 0
CLOSED --> OPEN : countDown()\nstate 减到 0
OPEN --> OPEN : countDown()\n无效果
OPEN --> OPEN : await()\n立即返回
state 只减不增,状态转换单向不可逆。这让 CountDownLatch 的实现极其简洁——Sync 内部类不到 50 行源码。
如果需要可重置的"栅栏"语义,应使用 CyclicBarrier。两者的选择不是"谁更好",而是"你的场景需要什么样的语义"。如果需要等待多个线程到达同一同步点后集体出发,用 CyclicBarrier;如果只需要等待 N 个事件发生,用 CountDownLatch。
设计理念 2:用 state 的值本身作为"是否开门"的判断
回顾 AQS 的 tryAcquireShared 模板方法——它的返回值有三种语义:
// tryAcquireShared 返回值约定:
// 负数 → 获取失败,入队等待
// 0 → 获取成功,但不传播唤醒
// 正数 → 获取成功,且需要传播唤醒给后续共享节点
CountDownLatch 对此的使用堪称精妙:
// CountDownLatch.Sync
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
// state=0 → 门闩打开 → 返回 1(成功+传播)
// state>0 → 门闩关闭 → 返回 -1(失败,入队)
}
不需要额外的布尔标志,不需要额外的锁, state 的值本身就是判断条件 。 state == 0 的意思是"开门",这个语义直接编码在 state 的数值上。
再看看 tryReleaseShared:
protected boolean tryReleaseShared(int releases) {
for (;;) {
int c = getState();
if (c == 0)
return false; // ① 已经是 0,不做任何事
int nextc = c - 1; // ② 减 1
if (compareAndSetState(c, nextc))
return nextc == 0; // ③ 减到 0 时返回 true,触发唤醒
}
}
return nextc == 0 是点睛之笔:只有当这次 countDown 恰好把计数器减到 0 时,才返回 true 通知 AQS 去唤醒等待者。 之前的所有 countDown(c > 1 时)返回 false,AQS 不会做唤醒操作。这意味着:
- N 次
countDown()中,前 N-1 次只修改 state,不触发唤醒 - 最后一次
countDown()才负责唤醒所有在await()上阻塞的线程
sequenceDiagram
participant T0 as 主线程
participant AQS as AQS
participant T1 as 子线程1
participant T2 as 子线程2
participant T3 as 子线程3
T0->>AQS: await()
AQS->>AQS: tryAcquireShared → -1\n(state=3,门闩关闭)
Note over T0: 入队 park 等待
T1->>AQS: countDown()
AQS->>AQS: CAS: state 3→2
Note over AQS: nextc != 0,不唤醒
T2->>AQS: countDown()
AQS->>AQS: CAS: state 2→1
Note over AQS: nextc != 0,不唤醒
T3->>AQS: countDown()
AQS->>AQS: CAS: state 1→0
Note over AQS: nextc == 0!触发 doReleaseShared
AQS->>T0: unpark
T0->>AQS: tryAcquireShared → 1\n(state=0,门闩打开)
Note over T0: 返回,继续执行
设计理念 3:共享模式的唤醒传播
CountDownLatch 选择 AQS 共享模式 的原因是:可能有多个线程同时在 await() 上等待——它们需要被同时唤醒。
flowchart TD
%% 半暗底色 + 高亮描边:完美适配博客深色/浅色双主题 %%
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb;
classDef data fill:#052e16,stroke:#16a34a,stroke-width:2px,color:#bbf7d0,font-weight:bold;
classDef condition fill:#2a1147,stroke:#a855f7,stroke-width:2px,color:#ede9fe,font-weight:bold;
A[最后一个 countDown\nstate 减到 0] --> B[doReleaseShared]
B --> C[unpark head.next]
C --> D[被唤醒线程执行 tryAcquireShared]
D --> E{state == 0?}
E -->|是| F[setHeadAndPropagate\n继续唤醒下一个]
F --> C
E -->|否| G[停止传播]
class E condition;
class B,D data;
class A,C,F,G process;
关键代码在 AQS 的 setHeadAndPropagate 中:
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared(); // 唤醒下一个共享节点
}
}
tryAcquireShared 返回 1(正数),满足 propagate > 0,因此唤醒会沿同步队列向后传播,直到所有等待者都被唤醒。这就是"一次 countDown 到 0,唤醒所有等待者"的底层机制。
这里有一个容易被忽略的设计细节: 为什么 tryAcquireShared 返回 1 而不是 0?
如果返回 0, propagate > 0 不成立,传播不会发生。在高并发场景下,可能有等待线程因为传播链断裂而永久阻塞。返回 1 确保传播一定会进行——这是 CountDownLatch 与 Semaphore ( tryAcquireShared 可能返回 0 表示最后一个许可被取走,后续无需传播)在设计意图上的根本区别。
设计理念 4:await 的超时与不可复用
await(long timeout, TimeUnit unit) 提供了超时等待能力。当超时发生时,线程被中断唤醒,AQS 的 doAcquireSharedNanos 将节点的 waitStatus 置为 CANCELLED,并从队列中移除。
但超时返回的线程 不会影响其他仍在等待的线程 ——state 没有变化,门闩仍然关闭。其他线程继续等待,直到 state 减到 0。
这意味着 CountDownLatch 天然支持"部分等待者超时退出,其余继续等待"的场景,不需要额外设计。
与 CyclicBarrier 的设计取舍对比
这两个工具类经常被放在一起比较,但它们的 设计意图完全不同 :
flowchart LR
%% 半暗底色 + 高亮描边:完美适配博客深色/浅色双主题 %%
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb;
classDef condition fill:#2a1147,stroke:#a855f7,stroke-width:2px,color:#ede9fe,font-weight:bold;
classDef startEnd fill:#701a4c,stroke:#e11d48,stroke-width:2.5px,color:#fce7f3,font-weight:bold;
root((线程协作工具))
root --> LATCH[CountDownLatch\n事件驱动]
root --> BARRIER[CyclicBarrier\n线程同步]
LATCH --> L1[一个线程等待\nN 个事件完成]
LATCH --> L2[一次性使用]
LATCH --> L3[基于 AQS 共享模式]
BARRIER --> B1[N 个线程互相等待\n全部到达后一起出发]
BARRIER --> B2[可循环使用]
BARRIER --> B3[基于 ReentrantLock + Condition]
class B3 condition;
class B1,B2,BARRIER,L2,L3,LATCH process;
class L1,root startEnd;
| 设计维度 | CountDownLatch | CyclicBarrier |
|---|---|---|
| 核心语义 | 等待"倒计时归零"这个事件 | 等待"所有线程到达"这个同步点 |
| 角色关系 | 等待者(被动)与倒计时者(主动)角色分离 | 所有参与者平等,互相等待 |
| 可重用性 | 一次性,state 只减不增 | 可循环,所有线程释放后自动重置 |
| 底层实现 | 继承 AQS,使用共享模式 | 组合 ReentrantLock + Condition |
| 触发条件 | state 减到 0 | 所有 N 个线程都调用了 await() |
| 中断处理 | 单个等待者被中断不影响其他等待者 | 一个线程被中断会破坏整个栅栏(BrokenBarrierException) |
| state 方向 | 单向递减 N → 0 | 单向递增 0 → N(到达计数) |
| 代码行数 | ~100 行(含 Sync) | ~400 行(含 Generation 代际管理) |
CyclicBarrier 的循环能力是以更高的实现复杂度为代价的。 它需要 Generation 对象来区分不同"代"的同步批次——当一代完成后,创建新的 Generation 开始下一轮。CountDownLatch 通过放弃循环能力,换取了极其简单的实现。
🌐 各自适用场景
// CountDownLatch 适用:主线程等待多个异步任务的结果
ExecutorService pool = Executors.newFixedThreadPool(5);
CountDownLatch latch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
pool.submit(() -> {
processOneItem();
latch.countDown(); // "我完成了"的信号
});
}
latch.await(); // 等待全部完成
System.out.println("10 个任务全部完成");
// CyclicBarrier 适用:多个线程分阶段协同计算
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("本轮计算完成,汇总结果");
});
for (int i = 0; i < 3; i++) {
new Thread(() -> {
for (int round = 0; round < 5; round++) {
computeOneRound();
barrier.await(); // 等待同伴,然后一起进入下一轮
}
}).start();
}
设计理念 5:极简 API 体现单一职责
CountDownLatch 只暴露两个核心方法,远少于其他 JUC 工具:
| 方法 | 职责 |
|---|---|
CountDownLatch(int count) | 构造,设定初始计数 |
countDown() | 计数器减 1 |
await() | 阻塞直到计数器 = 0 |
await(long, TimeUnit) | 带超时的阻塞等待 |
getCount() | 查询当前计数(仅用于调试/日志) |
没有 reset()、没有 increase()、没有 close()。每个方法职责单一,不重叠,不模糊。
这种 API 设计反映了 CountDownLatch 的 单一职责 :只负责倒计时这个任务。如果需要更复杂的操作(如动态增加计数、重置),说明你的场景不适合 CountDownLatch,应该用其他工具(如 Phaser)。
🛠️ 日常开发中的典型使用场景
// 场景1:微服务启动依赖检查
public class ServiceBootstrap {
public void start() throws InterruptedException {
CountDownLatch dbReady = new CountDownLatch(1);
CountDownLatch cacheReady = new CountDownLatch(1);
initDatabase(dbReady);
initCache(cacheReady);
dbReady.await();
cacheReady.await();
System.out.println("所有依赖就绪,开始接收请求");
}
}
// 场景2:并发测试工具——让所有线程同时出发
public class ConcurrentTest {
public void benchmark(int threadCount) throws InterruptedException {
CountDownLatch startGate = new CountDownLatch(1);
CountDownLatch endGate = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
try {
startGate.await(); // 等待发令枪
doWork();
} catch (InterruptedException e) { }
finally {
endGate.countDown();
}
}).start();
}
long start = System.nanoTime();
startGate.countDown(); // 发令枪
endGate.await(); // 等待所有线程完成
long elapsed = System.nanoTime() - start;
System.out.println("耗时: " + elapsed / 1_000_000 + " ms");
}
}
🎯 总结
flowchart TD
%% 半暗底色 + 高亮描边:完美适配博客深色/浅色双主题 %%
classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb;
classDef highlight fill:#431407,stroke:#ea580c,stroke-width:2px,color:#fed7aa,font-weight:bold;
classDef reject fill:#450a0a,stroke:#dc2626,stroke-width:2px,color:#fecaca,font-weight:bold;
subgraph CONSTRAINT[核心约束]
C1[一次性\nstate 只减不增]
C2[state==0\n就是开门信号]
C3[共享模式\n一次唤醒全部等待者]
end
subgraph RESULT[设计收益]
R1[实现简洁\nSync 不到 50 行]
R2[语义清晰\n无歧义状态]
R3[无需代际管理\n无 BrokenBarrierException]
end
subgraph TRADEOFF[设计取舍]
T1[不可重置 → 用 CyclicBarrier]
T2[不可回退 → 用 Semaphore]
T3[不可动态增加 → 用 Phaser]
end
CONSTRAINT --> RESULT
RESULT --> TRADEOFF
class CONSTRAINT highlight;
class C1,C2,C3,R1,R2,RESULT,T1,T2,T3,TRADEOFF process;
class R3 reject;
| 设计问题 | 答案 |
|---|---|
| 为什么是一次性的 | 消除"打开→关闭"的逆状态转换,避免代际管理,实现极简化 |
| 为什么用共享模式 | 多个等待者需要被同时唤醒,共享模式的传播机制天然适配 |
| state 为什么只减不增 | state 的值直接编码"是否开门"的语义,单向递减保证了不可逆性 |
| 为什么只有 2 个核心方法 | 单一职责——只做倒计时。复杂需求用 CyclicBarrier 或 Phaser |
| 与 CyclicBarrier 的本质区别 | Latch 等待事件(countDown 信号),Barrier 等待同伴(线程到达) |
| 前 N-1 次 countDown 为什么不唤醒 | 只有 state 恰好减到 0 的那次才返回 true 触发唤醒,之前都是无意义的提早唤醒 |