Java 并发同步器
Java 并发同步器用于协调多个线程何时继续执行。本文依次介绍 CountDownLatch、CyclicBarrier 和 Semaphore。它们分别用于等待事件完成、让参与者逐阶段会合,以及限制资源占用数量。掌握这三种用途后,再讨论超时、业务失败和屏障恢复。
1. 面试核心
先记住这几条,再读正文- 一次性计数。
CountDownLatch在计数归零后永久放行等待者,适合等待一组事件完成;同一个实例不能重置后复用。 - 循环屏障。
CyclicBarrier让固定数量的参与者逐轮会合,每轮成功后自动开始下一轮。卡 · CountDownLatch - 并发许可。
Semaphore用可重复获取和归还的许可,限制同时占用资源的任务数量。 - 屏障失败。
CyclicBarrier的等待者中断或超时时,会破坏本轮屏障并使其他等待者收到异常。卡 · 屏障的破坏与恢复 - 公平性的边界。
Semaphore.tryAcquire()即使在公平模式下也允许插队。
2. CountDownLatch
CountDownLatch 是一次性的倒计数器,适合等待一组任务结束。完成方每次递减计数,等待方在计数归零后继续执行。
CountDownLatch done = new CountDownLatch(tasks.size());
for (Task task : tasks) {
executor.execute(() -> {
try {
task.run();
} finally {
done.countDown();
}
});
}
if (!done.await(2, TimeUnit.SECONDS)) {
handleTimeout();
}
示例为每个任务分配一个计数,任务结束时调用 countDown()。一个或多个等待者可以调用 await()。计数归零后,当前等待者全部通过,后续 await() 也会立即返回。计数只减不增,因此重复等待新一批任务时需要创建新实例。
countDown() 放在 finally 中,保证任务抛异常时也会发送完成信号。如果提交被执行器拒绝,任务根本没有运行,finally 也就不会执行。调用方需要处理提交失败,明确这项计数由谁递减,或直接结束本次等待。
示例最多等待 2 秒,超时后 await() 返回 false。这时还要取消或接管剩余任务,避免后台工作无人管理。计数归零只说明完成信号齐全,业务成功结果与异常仍需另行收集。
3. CyclicBarrier
CyclicBarrier 称为循环屏障,让固定数量的线程在每一轮结束时互相等待。比如,并行计算需要所有线程算完本轮,才能汇总并开始下一轮。
CyclicBarrier barrier = new CyclicBarrier(workers, this::mergeRound);
每个参与者完成本阶段工作后调用 await()。最后到达的线程执行可选的 barrier action,也就是屏障放行前的汇总动作。示例中的动作是 mergeRound,成功结束后,本轮参与者全部放行。
屏障在成功会合后自动开始下一轮,因此可以重复使用。参与者数量动态变化时,可以用 Phaser 注册和注销参与者。若屏障失败,则需要协调所有参与者结束或重建本轮,具体见后文。
4. Semaphore
Semaphore 称为信号量,通过许可数量限制并发容量。许可表示一个可占用的资源名额,适合保护连接数、外部接口并发数或昂贵设备。
Semaphore permits = new Semaphore(20);
permits.acquire();
try {
callDownstream();
} finally {
permits.release();
}
acquire() 获取并消耗一个许可,许可不足时就等待。示例在获取成功后进入 try,再用 finally 归还。只要所有调用都遵守配对规则,就最多有 20 个调用同时进入下游。
Semaphore 没有线程所有权,所以许可也可以由另一个线程归还。重复调用 release() 会增加许可,甚至超过初始数量。因此,业务要明确每个许可由谁释放,并保证只归还一次。
5. 三者的选择
选择同步器时,先确认线程在等待什么。下表按完成事件、阶段会合和资源容量区分用途。
| 需求 | 组件 |
|---|---|
| 等待 N 个完成事件 | CountDownLatch |
| 固定参与者逐轮会合 | CyclicBarrier |
| 动态参与者逐阶段会合 | Phaser |
| 限制同时占用资源的数量 | Semaphore |
比如,主线程等待 N 个任务结束,可以用 latch 记录尚未完成的事件数。若要求最多 N 个任务同时调用接口,就需要 semaphore 管理可重复使用的名额。把 latch 用于限流,计数归零后就会永久放行,无法继续限制容量。
6. 超时与业务失败
同步器协调的是 JVM 内的线程,业务结果仍需调用方管理。设计失败路径时,要分别处理以下三类情况:
- 等待超时后,决定是否废弃迟到的结果,以及由谁取消或接管剩余任务。
- 获取许可后发生异常或中断,通过清理路径归还许可,让后续任务能继续进入。
- 屏障被破坏后,让参与者统一结束本轮,或在协调后重新开始。
内存计数随进程退出而消失,所以重启后恢复业务状态需要持久化记录。跨进程协调应使用数据库、消息系统或分布式协调服务,并由它们提供一致性保证。
7. 屏障的破坏与恢复
屏障需要所有参与者到齐,所以一个参与者退出会影响整轮计算。等待中的线程中断或超时时,会破坏本轮屏障。触发失败的线程分别收到 InterruptedException 或 TimeoutException。其他等待者则收到 BrokenBarrierException,barrier action 抛出异常也会破坏屏障。
参与者若在进入 await 前因业务异常退出,屏障无法自动感知。其余线程会继续等缺席者。可用带超时等待和统一取消协议结束本轮。
reset() 会打破当前等待,并开始新一轮屏障等待。重置前要协调旧参与者退出,否则上一轮的线程会混入新一轮。这样即使人数到齐,也无法保证它们处理的是同一阶段。
8. 常见问题
8.1 CountDownLatch 能重复使用吗?
不能重置后再用。CountDownLatch 的计数只减至零,后续等待会直接通过。固定参与者需要重复会合时,可使用 CyclicBarrier;参与者动态变化时,可选择 Phaser。
8.2 公平 Semaphore 会严格按请求顺序完成任务吗?
公平模式约束的是许可获取顺序,任务完成顺序还受调度和执行时长影响。它按线程到达内部排队点的顺序分配许可,而非外部请求到达的时刻。
无超时的 tryAcquire() 仍允许插队,包括一次获取多个许可的重载。批量获取还会影响等待时间:排在前面的任务需要多个许可时,要等数量足够才能继续。所以选择公平模式时,还要考虑任务各自需要多少许可。
9 · 面试题卡片
9.1 CountDownLatch、CyclicBarrier 和 Semaphore 有什么区别
出现公司:拼多多、阅文集团
考察重点
- 能根据等待的对象,区分事件完成、阶段会合和资源容量。
- 能解释 latch 为什么是一次性的,以及 barrier 怎样逐轮复用。
- 能说明超时或异常后由谁结束任务、处理屏障和归还许可。
相关内容:CountDownLatch至三者的选择。
参考回答
CountDownLatch 等待一组事件完成,CyclicBarrier 让固定参与者逐轮会合,Semaphore 限制并发资源占用。
latch 在计数归零后永久放行,所以适合一次性等待一批任务。barrier 在每轮参与者到齐后放行,再自动开始下一轮。semaphore 则重复发放和回收许可,让资源名额可以持续复用。
失败路径也随用途不同。latch 应在 finally 中递减计数,同时另行保存业务结果。barrier 需要协调本轮参与者一起退出或恢复。semaphore 则在成功获取许可后负责归还,避免容量泄漏或重复释放。
9.2 参与者业务失败会自动打破 CyclicBarrier 吗?
出现公司:暂无已核验的公开来源(本站自测)
考察重点:能区分进入 await 前的业务失败和等待中的失败,并解释重置前为何要协调参与者。
相关内容:屏障的破坏与恢复。
参考回答
参与者在进入 await() 前因业务异常退出时,屏障无法自动感知。
其余线程仍会等待缺席者,所以业务需要带超时的等待和统一取消协议。
等待中的线程中断或超时,以及 barrier action 抛出异常,会触发屏障的破坏机制。这时其他等待者收到 BrokenBarrierException。若要调用 reset() 重建屏障,应先协调旧参与者退出,避免新旧两轮混在一起。