CountDownLatch、CyclicBarrier、Semaphore 的区别?
一则或许对你有用的小广告
欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书
《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于
Spring AI + Spring Boot 3.x + JDK 21...,查看介绍《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...,查看介绍;演示链接:http://116.62.199.48:7070/《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/
新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍
截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观
面试考察点
-
同步语义理解:面试官不仅仅是想知道这三个类 API 的区别,更是想知道你是否理解它们分别解决 "等待完成"(
CountDownLatch)、"等待到齐"(CyclicBarrier)、"限流控制"(Semaphore)三种不同的协调语义。 -
底层实现差异:考察你是否知道
CountDownLatch和Semaphore基于 AQS,而CyclicBarrier基于ReentrantLock+Condition,理解这个差异是区分 "会用" 和 "懂原理" 的分水岭。 -
场景选型能力:给你一个实际场景,能不能迅速判断该用哪个?比如 "多线程并行处理任务后汇总结果" 用
CountDownLatch,"多轮比赛所有选手准备就绪后开赛" 用CyclicBarrier。
核心答案
一句话区分:CountDownLatch 是 "等 N 个任务做完"(一次性),CyclicBarrier 是 "等 N 个线程到齐后一起走"(可复用),Semaphore 是 "限流,最多允许 N 个线程同时执行"。
| 对比维度 | CountDownLatch |
CyclicBarrier |
Semaphore |
|---|---|---|---|
| 核心语义 | 等待 N 个事件完成 | N 个线程互相等待到齐 | 控制并发访问数量 |
| 计数方向 | 递减到 0,不可重置 | 递减到 0,自动重置 | 获取/释放许可证 |
| 可复用 | ❌ 一次性 | ✅ 自动重置(Cyclic) |
✅ 随时获取释放 |
| 底层实现 | AQS(state = 剩余次数) |
ReentrantLock + Condition |
AQS(state = 许可数) |
| 参与者角色 | 主线程等待,子线程 countDown |
所有线程都是对等的 | 获取许可的线程 |
| 典型场景 | 并行任务汇总、多服务初始化等待 | 多轮并行计算、多人游戏同步 | 限流、连接池、资源池 |
深度解析
一、用生活场景秒懂三者区别
三个类比应该很直观了:
-
CountDownLatch:裁判(主线程)等选手(子线程)跑完。选手不关心别人,跑完就走了。裁判等到所有人冲线才公布成绩。一次性的,比赛结束就没了。 -
CyclicBarrier:5 个人拼车,谁先到了都在等,最后一个人到了才一起出发。人人对等,没有 "裁判" 角色。到齐后自动重置,可以等下一轮(这就是Cyclic的含义)。 -
Semaphore:停车场就 3 个车位,先到先停,满了就在外面等,有人开走了就放一辆进来。控制的是同时进入的数量,和 "等待完成" 无关。
二、CountDownLatch——"一次性倒计时器"
// 典型场景:主线程等待 3 个子任务完成后汇总
CountDownLatch latch = new CountDownLatch(3);
// 3 个子线程并行执行
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
// 执行任务...
System.out.println(Thread.currentThread().getName() + " 完成");
} finally {
latch.countDown(); // 计数 -1,必须在 finally 里保证执行
}
}).start();
}
// 主线程等待
latch.await(); // 阻塞,直到 count 减到 0
System.out.println("所有任务完成,开始汇总");
底层原理:基于 AQS,state 初始化为 N,每次 countDown() 把 state 减 1(CAS),await() 检查 state == 0,不为 0 就进 CLH 队列挂起。当 state 减到 0 时,唤醒所有等待线程。
一个坑:如果某个子线程异常了没执行到 countDown(),await() 就会永远等下去。所以 countDown() 一定要放 finally 块里。
三、CyclicBarrier——"循环栅栏"
// 典型场景:多轮并行计算(如模拟赛跑预赛→复赛→决赛)
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
// 所有线程到齐后执行的回调(由最后到达的线程执行)
System.out.println("所有选手就位,发令枪响!");
});
for (int i = 0; i < 3; i++) {
new Thread(() -> {
for (int round = 1; round <= 3; round++) {
System.out.println(Thread.currentThread().getName() + " 第 " + round + " 轮准备就绪");
barrier.await(); // 等其他线程到齐,到齐后自动重置
// 所有线程同时继续执行
}
}).start();
}
底层原理:不基于 AQS!内部用 ReentrantLock + Condition 实现。每个线程调用 await() 时将计数减 1(count = parties - 已经 await 的线程数),如果不是最后一个,就 condition.await() 等待。最后一个线程到达时,signalAll() 唤醒所有人,并 自动重置计数,准备下一轮。
支持回调:构造函数可以传一个 Runnable,所有线程到齐后、继续执行前,会先执行这个回调。非常适合做 "阶段汇总"。
异常处理:如果某个线程在 await() 时被中断或超时,栅栏会被 "打破"(BrokenBarrierException),其他等待线程都会收到异常退出。可以调用 .reset() 手动重置。
四、Semaphore——"信号量/许可证"
// 典型场景:限制数据库连接数为 5
Semaphore semaphore = new Semaphore(5);
for (int i = 0; i < 20; i++) {
new Thread(() -> {
try {
semaphore.acquire(); // 获取许可证(剩余 -1),没有了就等
// 访问数据库...
System.out.println(Thread.currentThread().getName() + " 获取连接");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
semaphore.release(); // 释放许可证(剩余 +1),必须放 finally
}
}).start();
}
底层原理:基于 AQS,state 就是许可证数量。acquire() 尝试把 state 减 1,减到 0 就排队等待;release() 把 state 加 1,唤醒等待线程。
Semaphore 还支持公平模式:
// 公平信号量(按等待顺序获取)
Semaphore fair = new Semaphore(5, true);
五、三者核心差异对比
从 参与者角色 来看,CountDownLatch 存在明确的 "主从关系"——主线程调用 await() 等待,子线程各自 countDown() 完成就走人,子线程之间互不关心。CyclicBarrier 则是 "人人平等",所有线程都调用 await(),谁先到了就等,缺一个都不走。Semaphore 没有等待的概念,它只关心 "当前有几个线程在用资源"。
从 生命周期 来看,CountDownLatch 是一次性的,计数减到 0 就废了,想再用得重新 new。CyclicBarrier 到齐后自动重置计数,天然支持多轮循环。Semaphore 则随时 acquire/release,没有 "轮次" 的概念。
从 底层实现 来看,CountDownLatch 和 Semaphore 都基于 AQS,通过 state 变量 + CAS 实现计数和排队。CyclicBarrier 走的是另一条路,内部用 ReentrantLock + Condition 实现等待/唤醒机制,和 AQS 无关
六、怎么选?一张决策图搞定
- 你需要 一个线程等一堆线程完成?→
CountDownLatch - 你需要 一堆线程互相等齐了再一起走?→
CyclicBarrier - 你需要 限制同时执行的线程数量?→
Semaphore - 你需要 多轮重复的 "等齐再走"?→
CyclicBarrier(CountDownLatch是一次性的)
面试高频追问
-
CountDownLatch用完了能复用吗?不能。
state减到 0 就结束了,没有重置机制。如果需要重复使用,用CyclicBarrier,或者重新new一个CountDownLatch。 -
CyclicBarrier的BrokenBarrierException是怎么回事?如果某个线程在
await()时被中断、超时或者调用了reset(),栅栏会被 "打破",其他正在await()的线程会收到BrokenBarrierException。这是为了防止部分线程永远等下去。处理方式是reset()重建栅栏,或者终止所有线程。 -
Semaphore的acquire()和tryAcquire()有什么区别?acquire()会阻塞等待直到拿到许可证;tryAcquire()立即返回,拿不到就返回false,不阻塞。tryAcquire非常适合做限流——拿不到就返回 "系统繁忙",不让人在那傻等。
常见面试变体
- "
CountDownLatch和CyclicBarrier的区别?" - "什么场景用
Semaphore?" - "
CountDownLatch的底层实现原理?" - "
CyclicBarrier为什么能循环使用?"
记忆口诀
一句话区分:
CountDownLatch:一等多(一个主线程等 N 个子线程完成)CyclicBarrier:多等一齐(N 个线程互相等到齐,再一起走)Semaphore:限流(最多 N 个线程同时执行)
底层实现:CountDownLatch 和 Semaphore 基于 AQS,CyclicBarrier 基于 Lock + Condition
可复用:CountDownLatch 一次性,CyclicBarrier 可循环,Semaphore 随时获取释放
总结
CountDownLatch、CyclicBarrier、Semaphore 解决的是三种不同的线程协调问题:等待完成、等待到齐、限流控制。记住核心区别——"一等多" vs "多等一齐" vs "限流",再加上底层实现的差异(AQS vs Lock + Condition),这道题基本拿满分。选型的时候别纠结,看场景对号入座就行。
