CountDownLatch构造参数必须等于并发初始化的服务实例数,因计数器倒扣,设为1会导致主线程过早唤醒;countDown()须置于finally块确保异常时仍执行;await()必须设超时并检查返回值;其不可重用,重复使用应选CyclicBarrier。

CountDownLatch 构造参数为什么必须是服务数量,不能写死为 1
因为 CountDownLatch 的计数器是“倒扣”的,初始化时传入的值就是它等待的总次数。如果你有 3 个服务要初始化,就得传 new CountDownLatch(3);传 1 就意味着只要任意一个服务调完 countDown(),主线程就立刻唤醒——根本等不到其他服务。
常见错误现象:main thread resumed too early,日志显示某些服务的 init() 还没执行完,主线程已开始调用业务逻辑。
- 构造参数必须严格等于并发初始化的服务实例数(不是线程数)
- 如果服务列表动态生成,务必在创建
CountDownLatch前确定 size,例如new CountDownLatch(services.size()) - 不要在循环外提前 new,否则可能因服务数变化导致计数不匹配
每个服务怎么安全调用 countDown() —— 必须放在 finally 块里
初始化可能抛异常(比如数据库连不上、配置缺失),如果只在 try 块末尾调 countDown(),异常一出就跳过,计数器卡住,主线程永远阻塞在 await() 上。
正确做法是把 countDown() 放进 finally,确保无论成功失败都减一:
立即学习“Java免费学习笔记(深入)”;
service.init(); latch.countDown(); // ❌ 错误:异常时不会执行
try {
service.init();
} finally {
latch.countDown(); // ✅ 正确:无论如何都触发
}
- 即使
init()抛出RuntimeException或Error,finally仍会执行 - 不要在
catch里重试或吞异常后漏掉countDown() - 多个服务共用同一个
CountDownLatch实例,别各自 new
主线程 await() 要不要加超时?必须加
不设超时的 latch.await() 是不可控的:某个服务卡死、死锁、网络 hang 住,主线程就永久挂起,整个应用无法启动。生产环境绝对禁止无参 await()。
推荐用带超时的重载,并检查返回值:
if (!latch.await(30, TimeUnit.SECONDS)) {
throw new IllegalStateException("Services init timeout: " + latch.getCount() + " remaining");
}
- 超时时间建议略大于最慢服务的预期初始化耗时(如 30s),避免偶发延迟导致误判
- 检查
await()返回false后,用latch.getCount()查看还剩几个没完成,方便定位卡在哪 - 不要忽略返回值直接往下走——这是典型的“以为等到了,其实没等全”的坑
CountDownLatch 用完还能 reuse 吗?不能,且容易和 CyclicBarrier 混淆
CountDownLatch 是一次性用品。一旦计数归零,后续所有 await() 立即返回 true,countDown() 也不再有效。想重复使用,得重新 new 实例。
如果你需要“每次初始化都等齐、且能反复来”,说明场景其实更适合 CyclicBarrier——但它要求所有参与者都在同一轮里调 await(),不适合“主线程等子任务”这种主从模式。
- 初始化完成后,不要再对同一个
CountDownLatch实例做任何假设性操作 - 不要把它当全局单例长期持有,尤其在 Spring Bean 中被多处注入时,极易复用出错
- 调试时注意:IDE 断点停在
await()后,手动 step over 可能触发唤醒,掩盖真实阻塞问题
CountDownLatch 本身无法感知完成信号,必须靠回调里显式调 countDown(),而且得确保回调一定被执行——这已经超出同步等待的范畴了。


















