Java 并发实战:彻底搞懂 CyclicBarrier 与 CountDownLatch 的区别与应用场景
在日常 Java 后端开发过程中,处理并发任务是一个非常常见且关键的需求,尤其是在高性能系统或复杂流程协调中。JUC(java.util.concurrent)并发包中提供了许多强大的工具,其中 CyclicBarrier 和 CountDownLatch 是非常重要的两个组件。然而很多开发者对这两个工具的理解不够清晰,甚至在实际开发中傻傻分不清。
今天我们就通过业务实战案例来彻底讲清楚这两者的用法、区别、设计理念,并帮助你掌握它们在实际项目中的使用姿势。
一、CyclicBarrier:循环屏障,让线程“齐步走”
1.1 什么是 CyclicBarrier?
CyclicBarrier 直译为“循环栅栏”或“循环屏障”,它的核心作用是:让一组线程在某个同步点上全部等待,直到所有线程都达到这个点,才会同时继续执行后续操作。
可以将其想象成一个“线程集合点”或者“起跑线”:当所有线程都准备好了,才统一出发。
1.2 使用场景举例:期货交易系统统一上报
假设我们正在开发一个期货交易系统,期货交易对时间非常敏感,有严格的时间一致性要求。例如:
- 有一批期货交易数据:A、B、C 三个交易记录;
- 公司规定,这三个交易数据必须组成一个整体,同时向核心系统上报,不能有先后顺序;
- 否则可能会由于时间差,导致例如跨交易日的错误结算等严重问题。
业务原始处理逻辑(有问题的方式):
- 使用三个线程分别处理交易 A、B、C;
- A 线程 10 秒完成,B 线程 20 秒,C 线程 30 秒;
- 如果不加处理,A 线程就先把数据上报了,而 C 线程还没处理完;
- 结果:数据在不同时间点上报,存在时间差,违反业务要求。
如何解决:使用 CyclicBarrier
为了解决上述问题,我们可以使用 CyclicBarrier 来保证这三个线程在处理完各自任务后统一上报。
代码示例
CyclicBarrier barrier = new CyclicBarrier(3); // 创建一个屏障,等待3个线程
每个处理交易的线程逻辑如下:
// 线程处理交易A或B或C的业务逻辑...
doBusinessLogic();
// 处理完毕后调用 await() 进入阻塞状态
barrier.await();
核心逻辑解释:
- 每个线程处理完数据后,调用
barrier.await(); - 第一个线程调用时,程序在这里“卡住”;
- 第二个线程调用后,同样阻塞在 await 上;
- 当第三个线程也调用
await()后,触发屏障释放; - 此刻,三个线程“同时”通过屏障,继续向下执行统一上报的逻辑。
类比现实生活:
可以将 CyclicBarrier 比作赛道上的起跑线,三个选手(线程)准备好后,发令枪响,大家一起起跑。这保证了多个线程在逻辑上的“同时执行”。
1.3 CyclicBarrier 的使用特点
| 特性 | 说明 |
|---|---|
| 支持循环使用 | 调用 reset() 可重新使用相同的屏障 |
| 可设置屏障操作 | 构造时可传入 Runnable,在屏障释放前统一执行 |
| 适合“并发起步”场景 | 多线程处理需要统一起步,如批量上报、统一请求等 |
可选功能扩展:屏障任务
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("所有交易准备完毕,统一上报!");
});
当最后一个线程到达屏障点时,优先执行这个 Runnable,再放行线程。
二、CountDownLatch:倒计时门闩,控制顺序依赖关系
2.1 什么是 CountDownLatch?
CountDownLatch 被称为“倒计时锁存器”,用于一种典型的场景:
- 一个线程等待多个线程完成后再继续执行;
- 或者说,“主线程等待子线程完成任务后继续执行”。
它的机制就是维护一个计数器,调用一次 countDown() 就减 1,当减到 0 时,await() 的线程就会被唤醒继续执行。
2.2 使用场景举例:期货数据写入后的汇总操作
我们还是以交易 A、B、C 为例。这次业务需求稍有变化:
- A、B、C 各自由线程处理并写入数据库;
- 完成之后,系统要对所有交易数据进行统一汇总分析;
- 汇总任务必须在所有数据入库后才开始执行。
问题:
我们并不知道哪一个线程最慢,也不知道它们何时完成,无法确定汇总任务应该什么时候启动。
解决方案:使用 CountDownLatch
CountDownLatch latch = new CountDownLatch(3); // 初始化计数器为3
每个线程在业务处理完成后调用:
latch.countDown(); // 计数器减1
而汇总线程则在开始时调用:
latch.await(); // 阻塞等待,直到计数器为0
过程回顾:
- 每完成一个交易任务,调用一次
countDown(); - 汇总线程起初就调用了
await(),它会阻塞等待; - 当三个
countDown()执行完,计数器归零; - 汇总线程被唤醒,开始执行汇总逻辑。
这就构建了一种**“总-分任务”模型**:分任务先完成,总任务最后执行。
2.3 CountDownLatch 的使用特点
| 特性 | 说明 |
|---|---|
| 一次性使用 | 计数器归零后不能重置,只能用一次 |
| 适合“汇总/收尾”场景 | 需要等待多个子任务完成后执行下一步时使用 |
| 主线程先阻塞等待 | 调用 await() 的线程会一直等待直到计数归零 |
三、对比分析:CyclicBarrier VS CountDownLatch
| 特性 / 对比项 | CyclicBarrier | CountDownLatch |
|---|---|---|
| 主要用途 | 多线程“同步起跑” | 一个线程等待其他线程“全部完成” |
| 阻塞线程 | 每个参与线程都会调用 await() 阻塞 |
只有主线程或汇总线程调用 await() 阻塞 |
| 是否可复用 | 支持复用,可 reset | 一次性使用,不能 reset |
| 是否可设置统一操作 | 支持,构造时可传入 Runnable | 不支持 |
| 示例场景 | 并发请求发起、统一上报、压测模拟 | 系统启动准备、并发任务汇总、主从依赖场景 |
四、总结与使用建议
4.1 应该怎么选?
- 想要多个线程同时执行下一步?用
CyclicBarrier - 想要一个线程等其他线程处理完成后执行?用
CountDownLatch - 需要循环多次使用?只能选
CyclicBarrier - 只需要一次性同步?两个都可以,推荐
CountDownLatch
4.2 实战建议
| 场景 | 推荐使用 |
|---|---|
| 多线程任务处理完后需要统一进行合并、汇总 | CountDownLatch |
| 多个线程需要在统一时间点一起向服务器发起请求 | CyclicBarrier |
| 并发压测时模拟多个用户同时发起请求 | CyclicBarrier |
| 系统初始化阶段需要等待多个模块都准备完成再继续 | CountDownLatch |
五、结语
CyclicBarrier 和 CountDownLatch 都是并发编程中的“同步控制器”,虽然它们的 API 看起来相似,但在设计意图与应用场景上却有很大的区别。
通过今天的实际案例:
- 交易系统多线程上报 适合使用
CyclicBarrier实现“并发同步执行”; - 交易数据入库后汇总分析 适合使用
CountDownLatch实现“任务依赖控制”;
希望你在理解它们的原理和用法后,能更准确地在项目中应用这两个工具,写出更加健壮、安全、高并发的 Java 程序。
更多推荐

所有评论(0)