JUC 同步器(CountDownLatch / CyclicBarrier / Semaphore / Phaser / Exchanger)
JUC 的“同步器”解决的不是“互斥”(那是锁更擅长),而是 协作:让一组线程在某个时刻 一起开始 / 一起结束 / 分阶段推进 / 限流拿资源 / 成对交换数据。
如果你已经理解了 AQS(state + CLH 队列 + park/unpark),那同步器可以看成:
- 独占模式:常见于
Lock(互斥)。 - 共享模式:常见于门闩/信号量(多个线程可同时“通过”)。
1. 选型速查(面试 + 工程)
| 目标 | 首选同步器 | 关键点 | 常见坑 |
|---|---|---|---|
| 等所有子任务完成再继续 | CountDownLatch |
一次性;countDown() 递减到 0 后释放所有 await() |
忘记 finally countDown() 导致永远等待 |
| N 个线程相互等待后一起继续 | CyclicBarrier |
可复用;到达 parties 后触发 barrierAction | 任一线程中断/超时会 BrokenBarrier |
| 控制并发量/资源池许可证 | Semaphore |
permit 计数;公平/非公平 | acquire() 后异常未 release() → permit 泄漏 |
| 多阶段栅栏(阶段可动态变更) | Phaser |
支持 register/deregister;多 phase 推进 | 注册数管理错误导致永远推进不了 |
| 两线程配对交换数据 | Exchanger |
交换点 rendezvous;支持超时 | 线程数不成对/超时处理不当 |
2. CountDownLatch:一次性的“倒计时门闩”
2.1 语义
- 初始化一个计数
count。 - 线程调用
await():当count != 0就等待;当count == 0立刻通过。 - 线程调用
countDown():count--,直到减到 0 时唤醒所有等待者。
典型场景:
- 并行初始化(加载配置、预热缓存、建立连接)结束后再对外提供服务。
- 批量并行任务聚合(fan-out / fan-in)。
2.2 代码模板(推荐写法)
1 | ExecutorService pool = Executors.newFixedThreadPool(8); |
2.3 面试高频
CountDownLatch不可复用:计数到 0 后不能重置;需要复用用CyclicBarrier/Phaser。await()响应中断:被中断会抛InterruptedException,注意按规范Thread.currentThread().interrupt()回填标志。
3. CyclicBarrier:可复用的“集合点”
3.1 语义
- 设置
parties:需要到齐的线程数。 - 每个线程调用
await():到齐后 同时放行(并可执行一个 barrierAction)。 - “Cyclic”的含义:放行后,下一轮还可以继续使用。
3.2 与 CountDownLatch 的本质区别
CountDownLatch:等别人做完(一次性计数到 0)。CyclicBarrier:大家相互等(到齐再继续,可复用)。
3.3 代码模板
1 | CyclicBarrier barrier = new CyclicBarrier(3, () -> { |
3.4 易错点(很重要)
- 任一参与线程 中断/超时,barrier 会进入 broken 状态:
- 其他等待线程会收到
BrokenBarrierException。 - 后续调用
await()也会立即失败(除非reset())。
- 其他等待线程会收到
4. Semaphore:许可证(permit)= 并发量
4.1 语义
- 初始化 permit 数(比如 10)。
acquire():拿不到 permit 就阻塞;拿到则 permit–。release():permit++,并可能唤醒等待者。
典型场景:
- 资源池(连接池、限流、并发调用外部 API)。
- “最多同时 N 个线程进入临界区”(比锁更像“闸机”)。
4.2 工程模板:必须 finally release
1 | Semaphore sem = new Semaphore(10, true); // true: 公平(吞吐略低) |
4.3 面试高频
- 公平 vs 非公平:公平更少“饿死”,非公平吞吐更高(默认多为非公平)。
- permit 泄漏 是最常见生产事故:
acquire()后异常路径没release(),并发量会越来越小直到卡死。
5. Phaser:支持动态参与方的“阶段栅栏”
Phaser 可以理解为 更通用、更现代 的 CyclicBarrier:
- 支持多阶段(phase 0/1/2…)。
- 支持动态注册/注销参与者(
register()/arriveAndDeregister())。
5.1 典型场景
- 多阶段流水线:阶段 1 所有人完成 → 推进到阶段 2。
- 参与线程数在运行时变化(任务拆分、动态扩缩容)。
5.2 使用要点
- 每个阶段:参与者调用
arriveAndAwaitAdvance()。 - 退出必须
arriveAndDeregister(),否则 parties 计数不对会卡住。
1 | Phaser phaser = new Phaser(3); // 初始注册 3 个 party |
6. Exchanger:两个线程的“交换点”
6.1 语义
- 两个线程在交换点相遇:
- A 交出
a,得到 B 的b - B 交出
b,得到 A 的a
- A 交出
6.2 典型场景
- 双缓冲:生产者写满一块 buffer 后与消费者交换。
1 | Exchanger<List<String>> exchanger = new Exchanger<>(); |
6.3 易错点
- 线程数不成对 / 超时:必须设计好超时与重试/降级,否则会永久等待。
7. 总结:同步器与 AQS 的关系
CountDownLatch/Semaphore:典型 AQS 共享模式应用。CyclicBarrier:内部主要用ReentrantLock + Condition组织等待与唤醒(不是纯 AQS 子类那条路)。Phaser:更复杂的状态推进与等待组织(适合“阶段推进”的问题建模)。
如果你只记一个工程建议:
- “等待一组任务完成”优先用
CountDownLatch(配合finally countDown())。 - “分批同步推进、并且需要复用”优先用
CyclicBarrier/Phaser。 - “限并发/资源池”用
Semaphore。
面试题 / Checklist
CountDownLatch与CyclicBarrier的核心差异是什么?为什么一个能复用、一个不能?CyclicBarrier的 broken 状态何时发生?如何恢复/避免?Semaphore的 permit 泄漏在生产中如何预防?Semaphore的公平/非公平对吞吐与延迟有什么影响?Phaser相比CyclicBarrier的优势是什么?什么时候必须 deregister?Exchanger为什么需要“成对到达”?超时策略如何设计?
