CountDownLatch 原理
候选人小蒋在面试美团 P6 时,面试官问道:
"CountDownLatch 有什么用?它和 CyclicBarrier 有什么区别?"
小蒋说:"CountDownLatch 用于等待一组操作完成..."面试官追问:"它是怎么实现的?为什么叫倒计时门栓?"
小蒋答不上来。面试官继续:"countDown() 之后,await() 的线程一定会被唤醒吗?"
小蒋彻底卡住了...
一、核心问题:CountDownLatch 原理 🔴
1.1 问题拆解
第一层:使用场景(怎么用?)
"CountDownLatch 的典型使用场景是什么?"
考察点:主线程等待、多子任务并行
第二层:AQS 共享实现(怎么做到?)
"CountDownLatch 的 await() 和 countDown() 底层是怎么实现的?"
考察点:AQS 共享模式、state 递减
第三层:与 CyclicBarrier 的区别(有什么不同?)
"CountDownLatch 和 CyclicBarrier 的本质区别是什么?"
考察点:一次性 vs 可复用、谁等谁
第四层:局限性(有什么不能做的?)
"CountDownLatch 能被重置吗?"
考察点:一次性、不可复用
1.2 ❌ 错误示范
候选人原话 A:"CountDownLatch 和 CyclicBarrier 是一样的,只是名字不同。"
问题诊断:两者有本质区别。CountDownLatch 是一次性的,计数到零后不能重置;CyclicBarrier 是可复用的, parties 个线程全部通过后自动重置。
候选人原话 B:"CountDownLatch 计数到零后,所有等待的线程都会被唤醒。"
问题诊断:这个理解基本正确,但不够精确——只有调用了 await() 的线程会被唤醒。未调用 await() 的线程不受影响。
1.3 标准回答
P5 级别:使用场景
典型场景:主线程等待子任务完成
CountDownLatch latch = new CountDownLatch(3);
// 主线程等待
latch.await(); // 阻塞,直到计数为 0
// 子任务 1
new Thread(() -> {
doWork();
latch.countDown(); // 计数 -1
}).start();
// 子任务 2
new Thread(() -> {
doWork();
latch.countDown();
}).start();
// 子任务 3
new Thread(() -> {
doWork();
latch.countDown();
}).start();
常见使用场景:
- 启动多个服务,等待所有服务就绪后再开始处理
- 分布式计算中,主节点等待所有子节点完成计算后汇总结果
- 游戏加载:等待所有资源加载完成后进入游戏
P6 级别:AQS 共享实现
核心实现:
CountDownLatch 内部使用 AQS 的共享模式,state 字段存储倒计时计数:
// CountDownLatch.Sync
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
setState(count);
}
// tryAcquireShared: 计数为 0 才能获取
protected int tryAcquireShared(int ignores) {
return getState() == 0 ? 1 : -1;
}
// tryReleaseShared: 计数 -1,如果为 0 返回 true
protected boolean tryReleaseShared(int releases) {
for (;;) {
int c = getState();
if (c == 0) return false; // 已经是 0,不能再 release
int nextc = c - 1;
if (compareAndSetState(c, nextc))
return nextc == 0; // 返回 true 时唤醒等待线程
}
}
}
await() 的流程:
public final void acquireSharedInterruptibly(int arg) {
if (tryAcquireShared(arg) < 0) { // state != 0,获取失败
if (Thread.interrupted()) throw new InterruptedException();
doAcquireSharedInterruptibly(arg); // 阻塞,加入共享队列
}
}
countDown() 的流程:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) { // 递减 state,如果为 0
doReleaseShared(); // 唤醒共享队列中的所有节点
return true;
}
return false;
}
为什么叫"门栓":
"门栓"(Latch)是硬件中的一个概念——门栓关闭时阻止通过,门栓打开时允许通过。CountDownLatch 在计数未到零时阻止 await() 的线程通过,计数到零时打开门栓。
P7 级别:与 CyclicBarrier 的区别
本质区别:
CyclicBarrier 的内部实现:
public class CyclicBarrier {
private final ReentrantLock lock = new ReentrantLock();
private final Condition trip = lock.newCondition();
private final int parties;
private int count;
public int await() throws InterruptedException {
lock.lock();
try {
int index = --count;
if (index == 0) { // 最后一个到达
trip.signalAll(); // 唤醒所有等待线程
count = parties; // 重置
return 0;
}
// 等待其他线程
trip.await();
return index;
} finally {
lock.unlock();
}
}
}
使用对比:
// CountDownLatch:主线程等待子任务
// 场景:启动检查(多个服务就绪后主服务开始)
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch doneLatch = new CountDownLatch(SERVICE_COUNT);
for (int i = 0; i < SERVICE_COUNT; i++) {
final int id = i;
new Thread(() -> {
// 等待开始信号
startLatch.await();
service.start();
doneLatch.countDown();
}).start();
}
startLatch.countDown(); // 发出开始信号
doneLatch.await(); // 等待所有服务启动完成
// CyclicBarrier:多线程互相等待
// 场景:多线程并行处理数据块后汇总
CyclicBarrier barrier = new CyclicBarrier(N, () -> {
// barrierAction: 所有线程到达后执行
executor.execute汇总();
});
for (int i = 0; i < N; i++) {
final int id = i;
new Thread(() -> {
process(id);
barrier.await(); // 等待其他线程
continueWithResult(); // 所有线程到达后继续
}).start();
}
【面试官心理】
这道题我能问到 P7 级别,是因为它涉及了 AQS 共享模式和多线程同步原语的设计哲学。能说清 CountDownLatch 和 CyclicBarrier 本质区别的候选人说明他理解了不同同步场景的需求差异。能解释"一方等待另一方"vs"多方互相等待"的候选人说明他有实际的工程设计能力。
1.4 追问升级
追问 1:countDown() 在 await() 之后调用会发生什么?
如果 countDown() 时已经有线程在 await(),则调用 countDown() 的线程立即返回,doReleaseShared() 唤醒所有等待线程。剩余的 countDown() 调用也正常执行(state 继续递减,但不产生额外唤醒)。
追问 2:CountDownLatch 可以用于超时场景吗?
可以。使用 await(long timeout, TimeUnit unit) 或 awaitNanos(long nanosTimeout):
if (!latch.await(5, TimeUnit.SECONDS)) {
// 超时:部分任务未完成
}
二、生产避坑 🟡
2.1 计数设置错误
// 错误:计数为 0
CountDownLatch latch = new CountDownLatch(0);
latch.await(); // 立即返回,因为 state 初始为 0
// 正确:计数值 = 子任务数量
CountDownLatch latch = new CountDownLatch(taskCount);
2.2 countDown 未被调用
如果某个子任务因异常未调用 countDown(),await() 会永久阻塞。
解决:使用 try-finally 确保 countDown:
try {
doWork();
} finally {
latch.countDown(); // 即使异常也执行
}
三、CyclicBarrier 的额外特性 🟢
3.1 BrokenBarrier
如果某个线程在 await() 期间被中断,或 barrier 被 reset,所有正在等待的线程会抛出 BrokenBarrierException:
// 检测 barrier 是否损坏
if (barrier.isBroken()) {
// barrier 已损坏
}
3.2 CyclicBarrier vs Phaser
JDK 7 的 Phaser 是更灵活的屏障,支持动态注册参与方数量、多次同步点(比 CyclicBarrier 更强大):
Phaser phaser = new Phaser(3); // 初始 3 个参与方
phaser.arriveAndAwaitAdvance(); // 等待其他线程到达
💡
面试加分点:能说出"CountDownLatch 的 state 使用 volatile int 存储,但 compareAndSetState 使用 CAS 保证递减的原子性",说明他理解了状态管理的细节。
⚠️
面试陷阱:被问到"CountDownLatch 能被重置吗",答"能"是错的。CountDownLatch 是不可重置的。如果需要可重置的倒计时,使用 Phaser 或在每次使用前创建新的 CountDownLatch。