跳到主要内容
约 6 分钟2 张卡

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 会严格按请求顺序完成任务吗?​

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() 重建屏障,应先协调旧参与者退出,避免新旧两轮混在一起。

10. 参考资料​