Java并发三剑客:Semaphore、CountDownLatch与CyclicBarrier详解

发布时间:2026/8/2 4:04:29
Java并发三剑客:Semaphore、CountDownLatch与CyclicBarrier详解 面试官考点分析概念区分能否准确描述 Semaphore、CountDownLatch、CyclicBarrier 三者的核心作用与区别避免张冠李戴。底层原理是否理解它们都基于 AQS 实现以及各自如何利用共享锁/同步状态实现线程协调。使用方式能否正确写出典型用法并解释构造参数含义、常用方法及异常处理。实际落地是否在日常开发或主流框架如 Spring、Dubbo、Guava、线程池中真正使用过而非仅停留在理论层面。对比与选择是否清楚三者在可重用性、线程协作模式、适用场景上的差异并能针对不同业务需求做出合理选择。一、标准回答Semaphore信号量控制同时访问特定资源的线程数量通过许可证permits机制实现限流。典型用法new Semaphore(3)表示最多允许 3 个线程同时执行临界区代码。CountDownLatch倒计数门闩允许一个或多个线程等待直到在其他线程中执行的一组操作完成。计数器为 0 时所有等待线程被释放且不可重置。CyclicBarrier循环栅栏让一组线程相互等待直到到达某个公共屏障点barrier再继续执行且可重复使用。支持可选的Runnable回调在所有线程到达屏障时执行。一句话总结Semaphore 管理“资源个数”CountDownLatch 是“一次性门闩”CyclicBarrier 是“可复用的集结屏障”。二、核心原理2.1 共同基础AQS三者均基于AbstractQueuedSynchronizerAQS实现核心是一个 int 类型的state变量和 CLH 变体队列。Semaphore内部使用共享锁。state 表示剩余许可证数量acquire()通过 CAS 自旋尝试减少 state失败则入队阻塞。CountDownLatch同样使用共享锁。state 为计数器初始值countDown()通过 CAS 递减 state当 state 0 时唤醒队列中所有等待线程。CyclicBarrier基于ReentrantLock Condition实现并非直接单一 AQS。内部维护count剩余未到达线程数使用lock保护 count 变化并用trip.await()和trip.signalAll()实现线程阻塞与唤醒。2.2 工作原理图解Mermaid三、应用场景3.1 日常项目使用Semaphore数据库连接池、API 限流如限制同时调用外部接口的线程数不超过 50。CountDownLatch主线程等待多个子线程初始化完成如加载配置文件、预热缓存后再继续并行任务汇总结果。CyclicBarrier多线程计算单元每个线程负责一部分数据最后合并结果类似 MapReduce多人在线游戏等待所有玩家准备就绪。3.2 主流技术落地案例框架/中间件使用的工具典型落地场景DubboSemaphore服务提供者通过Semaphore控制最大并发请求数防止服务过载。Hystrix / SentinelSemaphore信号量隔离模式限制单个接口的异步并发量避免线程池资源耗尽。Guava RateLimiterSemaphore平稳/预热限流器内部使用类似信号量机制控制令牌发放。ZooKeeper 客户端 CuratorCountDownLatch等待主节点选举或集群初始化完成再启动业务线程。Spring Boot 启动器CountDownLatchSpring 容器刷新refresh()过程中等待各种初始化组件全部就绪。Kafka 消费端多线程协调CountDownLatch主线程等待所有消费者线程安全关闭后才退出 JVM。并行计算框架如 Fork/JoinCyclicBarrier子任务分阶段执行每个阶段所有子任务完成后再进入下一阶段。Excel 多 Sheet 并行导入CyclicBarrier多个线程分别处理不同 Sheet全部写入内存后统一数据校验。四、使用方式实例代码4.1 Semaphore 示例数据库连接池限流import java.util.concurrent.Semaphore; public class ConnectionPool { private final Semaphore semaphore new Semaphore(5); // 最多5个连接 public void executeTask() { try { semaphore.acquire(); // 获取许可证 // 模拟数据库操作 System.out.println(Thread.currentThread().getName() 获取连接); Thread.sleep(2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { semaphore.release(); // 释放许可证 System.out.println(Thread.currentThread().getName() 释放连接); } } } // 调用创建10个线程测试同时最多5个执行4.2 CountDownLatch 示例并行初始化组件import java.util.concurrent.CountDownLatch; public class AppStarter { public static void main(String[] args) throws InterruptedException { int n 3; CountDownLatch latch new CountDownLatch(n); for (int i 0; i n; i) { int id i; new Thread(() - { System.out.println(组件 id 初始化完成); latch.countDown(); }).start(); } latch.await(); // 主线程等待所有组件初始化完成 System.out.println(所有组件就绪启动服务); } }4.3 CyclicBarrier 示例分阶段并行计算import java.util.concurrent.CyclicBarrier; public class PhaseTask { static final int PARTIES 3; static CyclicBarrier barrier new CyclicBarrier(PARTIES, () - { System.out.println(本阶段完成执行汇总回调...); }); public static void main(String[] args) { for (int i 0; i PARTIES; i) { new Thread(new Worker(i)).start(); } } static class Worker implements Runnable { int id; Worker(int id) { this.id id; } public void run() { try { System.out.println(阶段一线程 id 正在计算); Thread.sleep((long) (Math.random() * 1000)); barrier.await(); // 等待其他线程 System.out.println(阶段二线程 id 继续计算); } catch (Exception e) { e.printStackTrace(); } } } }五、扩展延伸5.1 与 synchronized / Lock 的区别synchronized和ReentrantLock是排他锁一次只允许一个线程进入临界区而 Semaphore 允许多个线程并发。CountDownLatch / CyclicBarrier 不是锁是线程间协作工具关注点在于“到达某个同步点”。5.2 CountDownLatch vs CyclicBarrier对比维度CountDownLatchCyclicBarrier核心功能等待其他线程完成任务等待所有线程到达屏障可重用性不可重用一次性可复位重用回调执行无内置回调支持所有线程到达后自动执行 Runnable等待方角色允许部分线程等待await另一部分操作计数所有参与线程都必须在屏障处等待5.3 注意事项Semaphore 的acquire()和release()必须配对通常使用try-finally防止许可证泄漏。CountDownLatch 的countDown()调用次数必须足够否则主线程会在await()上被永久阻塞。CyclicBarrier 若某个线程在等待时异常或超时会破坏屏障其他线程可能抛出BrokenBarrierException需在await()中处理。六、面试追问深度篇追问Semaphore 公平与非公平模式在 AQS 源码层面有何本质差异公平模式FairSync在tryAcquireShared中会先调用hasQueuedPredecessors()检查 CLH 队列中是否有前驱节点如果有则返回 -1 进入排队保证严格 FIFO非公平模式NonfairSync则直接 CAS 抢锁不检查等待队列。非公平模式在高并发下虽然吞吐量更高但 AQS 中的插队会导致 CLH 队列尾部线程长期饥饿——这是因为新到达的线程直接和队头线程竞争队头即使被唤醒也可能抢不过新线程即惊群效应的变体。追问CountDownLatch 中 await() 和 countDown() 的调用顺序是否会影响正确性state 如何保证线程间可见调用顺序理论上不影响——即使主线程先调用await()进入阻塞随后其他线程再countDown()state 递减到 0 时同样会唤醒主线程。关键在于volatile CAS的内存语义AQS 内部的state是volatile intcountDown()通过 CAS 原子递减CAS 失败会自旋重试当 state 变为 0 时通过LockSupport.unpark()唤醒等待线程而unpark()操作与前的state写入存在 happens-before 关系保证等待线程醒来后能读到最新的 state0。但如果countDown()次数少于初始化计数主线程永久阻塞这是最常见的生产事故之一。追问CyclicBarrier 的 Generation代际机制是什么BrokenBarrierException 会在哪些场景下触发CyclicBarrier 内部维护一个Generation对象每次屏障被打破所有线程到达或屏障损坏都会创建新的 Generation 实例实现可重用。BrokenBarrierException 的触发场景有三个核心入口① 某个等待线程被中断Thread.interrupt()会调用breakBarrier()唤醒所有其他线程并抛出该异常② 调用await(long timeout, TimeUnit unit)超时同样触发breakBarrier()③ 调用reset()方法时如果仍有线程在await()上阻塞会先打破当前代际再创建新代际阻塞线程收到 BrokenBarrierException。面试深度点如果面试官追问如何优雅处理 BrokenBarrierException应回答——捕获后检查barrier.isBroken()若为 true 说明屏障已损坏且不会再自动恢复需要业务层做补偿逻辑如重新调度任务并在finally中确保不泄漏资源。追问Semaphore 的 release() 方法允许传入比 acquire() 更多的 permits 吗这会打破限流语义吗可以而且这正是 Semaphore 设计上需要特别注意的点。release()不检查调用线程是否持有许可证任何线程都可以调用release()——甚至可以传入比初始 permits 更大的值使得 state 超过初始上限。面试官可能进一步问那为什么不加持有者检查答为了保持高性能和灵活性——Semaphore 本身定位是计数信号量而非可重入锁如果每次 release 都要校验当前线程是否持有 permit需要额外维护一个线程-permit 映射开销远大于一个 CAS 操作。这也是为什么实际项目中建议用try-finally严格配对必要时可以在业务层封装一层带校验的 Semaphore 包装类。追问CyclicBarrier 配合线程池使用时如果线程池核心线程数小于 parties 数量会发生什么如何排查和解决直接死锁。假设 parties5 但线程池只有 3 个核心线程3 个线程执行到barrier.await()后进入等待但屏障永远等不到第 4、第 5 个线程到达——而线程池的 3 个线程又被阻塞无法释放去执行其他任务形成线程饥饿死锁。排查思路通过jstack打印线程堆栈会发现多个线程处于WAITING状态堆栈停留在CyclicBarrier.dowait()同时观察线程池的 activeCount 打满但无任务推进。解决与预防① 确保线程池线程数 ≥ parties 数量② 使用await(long timeout, TimeUnit unit)设置合理超时超时后打破屏障并记录告警避免永久阻塞③ 在 Spring Boot 等容器环境中结合Bean(destroyMethodshutdown)和awaitTermination实现优雅关闭防止屏障线程在 JVM 退出时被强制中断引发数据不一致。