HelloWorld CountDownLatch 指南
CountDownLatch 是一个“一次性门闩”:主线程或若干线程等待其他线程完成指定次数的事件,事件由其他线程调用 countDown() 触发。适合并发启动后汇总结果的场景,不能重用,若需循环使用请考虑 CyclicBarrier 或 Phaser,本指南通过 HelloWorld 示例、源码要点、常见陷阱与替代方案,逐步带你理解并正确使用它。

先把概念说清楚:CountDownLatch 是什么?
想象门口有一把闸门,需要收集 N 个钥匙才能打开。CountDownLatch 就像那把闸门:构造时给定一个计数器(N),等待方调用 await() 阻塞直到计数归零;完成方每做完一件事就调用 countDown(),直到所有钥匙到位,门开了,等待线程继续执行。
核心要点(一句话版)
- 一次性:构造好的 CountDownLatch 计数归零后不可重置。
- 线程安全:基于 AQS(AbstractQueuedSynchronizer)实现,支持并发调用。
- 阻塞与中断:await() 可以被中断并抛出 InterruptedException,也支持带超时的 await(long, TimeUnit)。
主要 API 快速参照
- CountDownLatch(int count):创建一个初始计数为 count 的闩。
- await():阻塞当前线程直到计数为 0 或线程被中断。
- await(long, TimeUnit):带超时的等待,超时返回 false。
- countDown():将计数器减 1;若减到 0,释放所有等待线程。
- getCount():返回当前计数。
HelloWorld 实战示例(一步步来)
我们做一个很直观的例子:主线程启动若干个 worker,每个 worker 做点事(模拟耗时),做完后调用 countDown,主线程等待所有 worker 完成再打印 “Hello World”。这样就能观察 CountDownLatch 的典型使用场景。
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class HelloWorldLatch {
public static void main(String[] args) throws InterruptedException {
int workers = 3;
CountDownLatch latch = new CountDownLatch(workers);
ExecutorService es = Executors.newFixedThreadPool(workers);
for (int i = 0; i < workers; i++) {
final int id = i;
es.submit(() -> {
try {
// 模拟任务
TimeUnit.SECONDS.sleep(1 + id);
System.out.println("worker " + id + " done");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown();
}
});
}
// 等待所有 worker 完成
latch.await();
System.out.println("Hello World");
es.shutdown();
}
}
运行与观察
- 你会看到 worker 按耗时打印 “worker X done”。
- 当最后一个 worker 调用 countDown 后,主线程从 await 返回并打印 “Hello World”。
- 如果任何 worker 在 finally 中缺少 countDown,主线程会永久等待(除非使用带超时的 await)。
背后的实现要点(为什么它能工作的)
CountDownLatch 内部使用 AbstractQueuedSynchronizer(AQS)维护一个 int 状态作为计数器。await() 会以共享模式尝试获取“许可”,如果计数 > 0,就会被入队等待;countDown() 则通过 CAS 操作减小状态,若减到 0,则调用 releaseShared,唤醒等待队列中的所有线程。
- 可见性:由于使用了 AQS 的内存屏障,countDown 对 await 的可见性被保证(不会出现长期可见性问题)。
- 中断策略:await() 响应中断,会抛 InterruptedException,但 countDown 不会响应中断。
- 没有公平性选项:CountDownLatch 没有像 ReentrantLock 的公平性设置,它更轻量,设计目标明确。
常见误区与坑
- 以为可重用:误用最常见——期望同一个 CountDownLatch 在一次等待后还能再用。它不能重置,除非新建实例。
- 忘记 countDown:在任务异常时没在 finally 中调用 countDown,会导致死等待。
- 误用为同步计数器:不要用 CountDownLatch 来计数资源或并发许可(这不是它的目的);Semaphore 才适合控制并发许可数。
- 以为 await 会无限阻塞不可中断:await 可被中断并抛异常,调用方要设计中断处理。
何时用它:适用场景
- 并行初始化:多个模块并行初始化,主线程需等待全部完成再继续。
- 多线程测试等待:测试代码中等待若干并发任务完成再断言。
- 简单的一次性汇总:主线程等待若干 worker 运行完毕并汇总结果。
与其他并发工具的比较(快速参考)
| CountDownLatch | CyclicBarrier | Phaser | Semaphore | |
| 是否可重用 | 否 | 是(可循环屏障) | 是(更灵活) | 是(许可可多次获取/释放) |
| 典型场景 | 一次性等待 | 多线程在阶段性点同步 | 分阶段的动态线程参与 | 控制并发许可数量 |
| 支持动态加入 | 否 | 否 | 是 | 部分(通过许可变化) |
调优与最佳实践清单
- 始终在 finally 中调用 countDown(),避免异常导致的死锁。
- 如果需要重复使用,使用 CyclicBarrier 或 Phaser。
- 对外部可中断的等待,处理 InterruptedException:恢复中断状态或做退避处理。
- 避免把 CountDownLatch 用作高频短生命周期的同步点以外的大量对象创建,必要时重用线程池而非重复创建线程。
- 对超时敏感的场景,使用带超时的 await 并设计超时回退逻辑。
调试技巧:当等待不返回时怎么办?
- 打印 getCount() 的值(或在日志中记录关键路径),确认计数是否在预期中下降。
- 检查所有路径是否保证在结束时调用 countDown(尤其是 catch 块和异常路径)。
- 使用线程 dump(jstack)查看哪些线程在 await,哪些线程被阻塞或死掉。
- 临时把 await 替换为带超时的 await,观察是否超时触发并记录更多上下文。
进阶话题(想更深一点)
如果你对并发原理有兴趣,进一步可以看看 AQS 的源码,理解 CountDownLatch 如何通过 state 管理同步,以及 releaseShared/acquireShared 的实现细节。推荐读物包括《Java Concurrency in Practice》与 Doug Lea 的相关论文与源码注释,这些会把“为什么”解释得更透彻。
说到这儿,感觉像是把工具箱的一个螺丝刀拆开讲清楚了。用 CountDownLatch,通常是很简洁有效的:启动、等待、汇总。但也要记得它的限制和替代品,写并发代码,少一点臆测,多一点边界条件与异常处理,日子会好过一点。