CountDownLatch / CyclicBarrier / Semaphore:三个 AQS 同步工具的区别与实战
问题
CountDownLatch、CyclicBarrier、Semaphore 这三个并发工具类都出自 java.util.concurrent 包,它们都基于 AQS 实现,但从使用方式到适用场景都截然不同。很多开发者只背过它们的"定义"和"经典示例",到了真要用的时候却选错工具——比如用 CountDownLatch 做循环同步,或者用 Semaphore 当计数器用。本文从源码、工作流程、实战场景三个维度拆解这三者的本质。
核心答案
1. CountDownLatch(倒计时器)
构造时指定一个初始计数 N,一个或多个线程调用 await() 阻塞等待,直到其他线程调用 countDown() 将计数减到 0 为止。
关键特征:一次性使用,不能重置。
// 示例:主线程等待 3 个任务全部完成
CountDownLatch latch = new CountDownLatch(3);
// 三个工作线程
for (int i = 0; i < 3; i++) {
int taskId = i;
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(taskId + 1);
System.out.println("任务 " + taskId + " 完成");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown(); // 务必在 finally 中调用
}
}).start();
}
// 主线程等待
latch.await(); // 阻塞直到计数归零
System.out.println("所有任务完成,主线程继续");2. CyclicBarrier(循环栅栏)
构造时指定一个参与线程数 N,每个线程执行到 await() 时阻塞,当 N 个线程都到达栅栏后,所有线程同时释放,可循环使用。
关键特征:可重置(reset()),所有线程互相等待,支持可选的 barrierAction(最后一个到达的线程触发)。
// 示例:3 个线程分阶段并行计算,每阶段同步一次
CyclicBarrier barrier = new CyclicBarrier(3, () ->
System.out.println("=== 阶段完成,进入下一阶段 ===")
);
for (int i = 0; i < 3; i++) {
int threadId = i;
new Thread(() -> {
try {
// 阶段 1
System.out.println("线程 " + threadId + " 完成阶段 1");
barrier.await(); // 等待其他线程
// 阶段 2
System.out.println("线程 " + threadId + " 完成阶段 2");
barrier.await(); // 再次等待,循环使用
System.out.println("线程 " + threadId + " 结束");
} catch (InterruptedException | BrokenBarrierException e) {
Thread.currentThread().interrupt();
}
}).start();
}3. Semaphore(信号量)
构造时指定许可数 N,线程通过 acquire() 获取许可(没有则阻塞),release() 归还许可。本质是一个限流器,控制同时访问资源的线程数。
关键特征:许可可复用,支持公平/非公平模式,acquire 和 release 必须成对出现。
// 示例:限流,只允许 2 个线程同时访问资源
Semaphore semaphore = new Semaphore(2, true); // 公平模式
for (int i = 0; i < 5; i++) {
int threadId = i;
new Thread(() -> {
try {
semaphore.acquire();
System.out.println("线程 " + threadId + " 获取到许可,开始工作");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
semaphore.release(); // 务必在 finally 中释放
}
}).start();
}三者的核心区别
| 维度 | CountDownLatch | CyclicBarrier | Semaphore |
|---|---|---|---|
| 语义 | 等待倒计时归零 | 线程互相等待到齐 | 获取/归还许可 |
| 可重用 | ❌ 一次性 | ✅ 可循环 | ✅ 一直可用 |
| 谁触发 | 外部线程 countDown() | 参与线程自己 await() | 线程自己 acquire()/release() |
| AQS 模式 | 共享锁 | 独占锁(内部用 ReentrantLock + Condition) | 共享锁 |
| 典型场景 | 主线程等待子任务 | 分阶段并行计算 | 限流、资源池化 |
源码分析:AQS 层面的差异
CountDownLatch 的 AQS 实现
CountDownLatch 内部有一个继承 AQS 的内部类 Sync,将初始计数设置为 AQS 的 state:
countDown()→ 调用 AQS 的releaseShared(1)→tryReleaseShared对 state 做 CAS 减 1,减到 0 时调用doReleaseShared()唤醒所有等待线程。await()→ 调用 AQS 的acquireSharedInterruptibly(1)→tryAcquireShared检查state == 0,不为 0 则入队等待。
核心逻辑:计数归零时一次唤醒所有等待者,这是典型的共享锁 release 模式。
CyclicBarrier 的生成机制
CyclicBarrier 不是直接基于 AQS,而是组合了 ReentrantLock + Condition。内部维护一个 Generation 对象来区分轮次:
await()调用dowait():获取锁,检查count减 1 后是否为 0。- 最后一个到达的线程:执行
barrierAction(如果有),然后调用nextGeneration()唤醒所有等待线程,重置count和generation。 - 非最后到达的线程:调用
Condition.await()阻塞,等待最后一个线程唤醒。
注意:reset() 会抛出 BrokenBarrierException,使用方必须处理这个异常。如果某个线程在 await() 期间被中断或超时,栅栏会进入"broken"状态,所有其他线程都会收到 BrokenBarrierException。
Semaphore 的公平/非公平
Semaphore 内部也有 Sync 继承 AQS,state 表示剩余许可数:
acquire()→acquireSharedInterruptibly(1)→tryAcquireShared尝试减少 state,非公平版直接 CAS(compareAndSetState),公平版先检查hasQueuedPredecessors()。release()→releaseShared(1)→tryReleaseShared对 state CAS 加 1,成功则调用doReleaseShared()唤醒等待线程。
非公平模式:新线程到达时,直接 CAS 抢许可,可能插队到已经在队列中等待的线程前面,吞吐更高但存在饥饿风险。公平模式:严格 FIFO,队列中有等待者就入队,不插队。
生产实战:选型指南
什么场景用 CountDownLatch
- 主线程等待 N 个并行服务初始化完成后再接受请求
- 批量 RPC 调用,等待所有结果返回后统一处理
- 压测工具中,等待所有线程准备就绪后同时开始
坑点:countDown() 必须在 finally 块中调用,否则异常导致计数永远不归零,所有 await() 线程永久阻塞。
什么场景用 CyclicBarrier
- 多线程分阶段计算,每阶段各自算完再同步进入下一阶段(如 MapReduce 的 shuffle 阶段)
- 模拟并发请求,所有线程"同时"发起请求(用
await()对齐时间点)
坑点:reset() 要谨慎使用,会抛出 BrokenBarrierException;await() 需要设置超时,防止线程永久阻塞。
什么场景用 Semaphore
- 数据库连接池限流:控制同时获取连接的线程数
- 接口 QPS 整形:控制同时处理的请求数
- 有限资源访问控制:如打印机、文件句柄
坑点:acquire 和 release 必须成对出现(finally 中 release),否则泄漏一个许可就永久少一个。获取多个许可时(acquire(N))更要小心,释放数量必须一致。
总结
三个工具的本质区别可以一句话记住:CountDownLatch 是"等结果",CyclicBarrier 是"等队友",Semaphore 是"等票"。它们都基于 AQS,但 AQS 的 state 在不同类中承载了完全不同的语义——CountDownLatch 用 state 做倒计数,Semaphore 用 state 做许可池,而 CyclicBarrier 甚至绕过了 AQS 直接用了 ReentrantLock + Condition。理解这些底层的差异,才能在面试和真实场景中选对工具、写对代码。