在并发编程中,我们经常遇到这样的场景:主线程把一个大任务拆分成若干子任务交给线程池执行,需要等所有子任务都跑完之后再汇总结果;或者一个服务启动时,需要等待配置加载、缓存预热、资源初始化等多个前置动作全部就绪后才能对外提供服务。这类需求本质上就是一个同步计数问题,而 JDK 中提供的 CountDownLatch 正是为此而生的利器。它结构简单、语义清晰,是 Java 并发工具包里使用频率最高的组件之一。

CountDownLatch 的核心概念与工作原理
CountDownLatch 直译过来就是倒计时锁存器,它的内部维护了一个整数计数器。初始化时传入一个数值 N,每调用一次 countDown 方法计数器就减一,而调用 await 方法的线程会被阻塞,直到计数器归零才能继续执行。可以把它想象成一道闸门:N 个人每人都握着一个开关,只有所有人都按下了开关(计数归零),闸门才会打开。
从源码角度看,CountDownLatch 的实现完全依赖于 AbstractQueuedSynchronizer(AQS)。它把 AQS 的 state 变量当作计数器使用:构造时通过 setState 设置初始值,countDown 内部调用的是 AQS 的 releaseShared 操作,利用 CAS 把 state 减一,减到零时会唤醒所有在同步队列中等待的线程;await 则调用 acquireSharedInterruptibly,state 不为零就把当前线程挂起排队。理解了这一点,你就能明白为什么 CountDownLatch 的行为是这样设计的。
需要特别注意的是,CountDownLatch 是一次性的。计数器一旦归零就永远保持为零,这个锁存器无法被重置复用。如果业务中需要循环多次使用屏障,应该考虑 CyclicBarrier 而不是 CountDownLatch。
典型使用场景与完整代码示例
最常见的用法是主线程等待多个工作线程完成汇总。下面给出一个完整的例子:假设要统计一个目录下三类文件的行数,主线程等待三个统计线程全部完成后输出总数。
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class LineCountDemo {
public static void main(String[] args) throws InterruptedException {
// 计数器初始值为 3,对应 3 个子任务
CountDownLatch latch = new CountDownLatch(3);
// 用一个数组收集结果,主线程最后汇总
long[] results = new long[3];
ExecutorService pool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 3; i++) {
final int index = i;
pool.submit(() -> {
try {
// 模拟每类文件的行数统计,实际中替换为真实逻辑
results[index] = doCount(index);
} finally {
// 必须放在 finally 中,防止异常导致计数不减
latch.countDown();
}
});
}
// 主线程阻塞等待,直到计数归零
latch.await();
pool.shutdown();
long total = results[0] + results[1] + results[2];
System.out.println("三类文件总行数: " + total);
}
private static long doCount(int type) {
try {
Thread.sleep(1000); // 模拟耗时操作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return 100L * (type + 1);
}
}
上面这段代码有几个值得强调的细节。第一,countDown 必须放在 finally 块中,否则一旦子任务抛出异常,计数器永远无法归零,主线程会一直卡死在 await 上,这是生产事故的高发点。第二,await 支持带超时的重载版本 await(long timeout, TimeUnit unit),建议在对外服务中使用它,避免某个子任务卡住拖垮整个请求线程。第三,汇总结果用的是普通数组,因为每个线程写入不同的下标,不存在竞争;如果多个线程要写同一个共享变量,就需要改用 AtomicInteger 或者加锁。
另一个典型场景是服务启动时的资源初始化,或者接口聚合:一个 BFF 接口需要同时调用三个下游服务再合并返回,用 CountDownLatch 加线程池可以把串行调用改成并行,整体耗时从三个接口之和降到最慢的那个接口的耗时,效果非常明显。
CountDownLatch 与 join、CyclicBarrier 的对比
Thread 的 join 方法也能实现等待,但它有明显的局限:join 只能等待线程对象本身结束,如果任务提交给了线程池,你拿不到也管理不了线程对象,join 就无从谈起。而 CountDownLatch 等待的是一个事件信号,不管这个计数是由谁在哪个线程里递减的,解耦程度更高。
与 CyclicBarrier 相比,两者的区别主要在语义和复用性上。CyclicBarrier 强调的是所有线程互相等待,到齐后一起继续,并且可以通过 reset 重置循环使用;CountDownLatch 强调的是一方等待多方完成,且不可重置。另外 CyclicBarrier 还支持在屏障打开时执行一个回调动作,适合做阶段性的批处理。简单记忆:等待别人干活用 CountDownLatch,大家互相等齐再走用 CyclicBarrier。
使用时还有几个坑要避开。一是计数器初始值与实际 countDown 次数必须严格一致,多减一次虽然不报错但会破坏语义,少减一次则会永久阻塞;二是 await 响应中断,如果等待线程被中断会抛出 InterruptedException,需要妥善处理中断状态;三是不要在持有锁的情况下调用 await,容易与其他线程的 countDown 形成死锁。掌握这些原则,CountDownLatch 就能成为你手里可靠的并发协调工具。
CountDownLatch多线程同步Java并发编程修改时间:2026-09-16 11:48:33