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 代码模板(推荐写法)¶
ExecutorService pool = Executors.newFixedThreadPool(8);
CountDownLatch latch = new CountDownLatch(tasks.size());
for (Runnable task : tasks) {
pool.execute(() -> {
try {
task.run();
} finally {
// 关键:保证一定能走到
latch.countDown();
}
});
}
boolean ok = latch.await(3, TimeUnit.SECONDS);
if (!ok) {
// 超时:记录未完成项、降级、报警等
}
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 代码模板¶
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
// 可选:最后到达的线程执行 barrierAction
System.out.println("all arrived");
});
Runnable worker = () -> {
try {
doStep();
barrier.await(2, TimeUnit.SECONDS);
doNextStep();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (TimeoutException e) {
// 超时会导致 barrier broken
} catch (BrokenBarrierException e) {
// 一旦 broken,后续 await 都会失败,除非 reset
}
};
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¶
Semaphore sem = new Semaphore(10, true); // true: 公平(吞吐略低)
public void callRemote() throws InterruptedException {
sem.acquire();
try {
doRpc();
} finally {
sem.release();
}
}
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 计数不对会卡住。
Phaser phaser = new Phaser(3); // 初始注册 3 个 party
Runnable stepper = () -> {
doPhase0();
phaser.arriveAndAwaitAdvance();
doPhase1();
phaser.arriveAndAwaitAdvance();
phaser.arriveAndDeregister();
};
6. Exchanger:两个线程的“交换点”¶
6.1 语义¶
- 两个线程在交换点相遇:
- A 交出
a,得到 B 的b - B 交出
b,得到 A 的a
6.2 典型场景¶
- 双缓冲:生产者写满一块 buffer 后与消费者交换。
Exchanger<List<String>> exchanger = new Exchanger<>();
// producer
List<String> filled = fill();
List<String> empty = exchanger.exchange(filled, 1, TimeUnit.SECONDS);
// consumer
List<String> toConsume = exchanger.exchange(emptyBuf, 1, TimeUnit.SECONDS);
consume(toConsume);
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为什么需要“成对到达”?超时策略如何设计?