CountDownLatch 解决的是一个非常具体的并发协调问题:一个(或多个)线程需要等待另外 N 个线程全部完成后,才能继续执行。典型如首页聚合接口:并行调用用户服务、订单服务、推荐服务,三个都返回后才能组装响应。本文从用法到 AQS 源码层把原理讲透。

用法模型

CountDownLatch latch = new CountDownLatch(3);

for (String svc : List.of("user", "order", "recommend")) {
pool.submit(() -> {
try {
call(svc); // 并行发起远程调用
} finally {
latch.countDown(); // 完成一个,计数减一
}
});
}

latch.await(); // 主线程阻塞,直到计数归零
assemble(); // 三个结果都就绪,组装响应

两个核心方法:countDown() 把计数器减一(减到 0 时释放所有等待者);await() 让当前线程阻塞直到计数器为 0。注意 countDown 必须放在 finally 里——任务抛异常若不计数,await 将永久阻塞,这是生产上最常见的 CountDownLatch 事故。

原理:AQS 共享模式

CountDownLatch 内部是一个继承 AQS(AbstractQueuedSynchronizer)的 Sync:

  • state 即计数器:构造时 setState(N)
  • countDown() → releaseShared(1) → CAS 把 state 减一;减到 0 时返回 true,触发 doReleaseShared() 唤醒等待队列中所有线程
  • await() → acquireSharedInterruptibly(1):state 不为 0 就把当前线程包装成节点加入 CLH 等待队列并 park 阻塞;被唤醒后重新检查 state,为 0 才通过

关键点在于它是共享模式:与 ReentrantLock 的独占模式不同,共享模式允许”一次释放、唤醒全部等待线程”,这正是”门闩打开、所有人通过”的语义。

与 CyclicBarrier 的区别

维度 CountDownLatch CyclicBarrier
等待关系 一个线程等 N 个事件 N 个线程互相等待到齐
复用 一次性,归零后失效 可循环重用(reset)
回调 无 到齐时可执行 barrierAction
实现 AQS 共享模式 ReentrantLock + Condition

选型口诀:等”事情做完”用 latch,等”人到齐一起出发”用 barrier。

几个容易踩的坑

  • 计数不匹配:new 了 3 但只提交 2 个任务(或某任务异常跳过 countDown),await 线程死等。建议 await 用带超时版本 await(timeout, unit) 兜底
  • 重复使用:归零后再 countDown 无效、再 await 直接通过,需要重复协调应换 CyclicBarrier 或每次新建 latch
  • await 线程被中断:抛 InterruptedException,需决定是重试还是降级返回部分结果

延伸:CompletableFuture 的替代

JDK8 之后,同样的聚合场景更推荐 CompletableFuture:

CompletableFuture.allOf(
supplyAsync(this::user, pool),
supplyAsync(this::order, pool),
supplyAsync(this::recommend, pool)
).join();

它把”等待+取结果”合为一体,还自带异常传播与编排能力。CountDownLatch 的价值在于语义直白、无返回值协调(如等待 N 个初始化动作完成)时依然最清晰。

理解 CountDownLatch 的关键是抓住”AQS 共享模式 + state 作计数器”这一本质:其余 API 都只是这两个事实的包装。