在日常 Java 后端开发过程中,处理并发任务是一个非常常见且关键的需求,尤其是在高性能系统或复杂流程协调中。JUC(java.util.concurrent)并发包中提供了许多强大的工具,其中 CyclicBarrierCountDownLatch 是非常重要的两个组件。然而很多开发者对这两个工具的理解不够清晰,甚至在实际开发中傻傻分不清。

今天我们就通过业务实战案例来彻底讲清楚这两者的用法、区别、设计理念,并帮助你掌握它们在实际项目中的使用姿势。


一、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

五、结语

CyclicBarrierCountDownLatch 都是并发编程中的“同步控制器”,虽然它们的 API 看起来相似,但在设计意图与应用场景上却有很大的区别。

通过今天的实际案例:

  • 交易系统多线程上报 适合使用 CyclicBarrier 实现“并发同步执行”;
  • 交易数据入库后汇总分析 适合使用 CountDownLatch 实现“任务依赖控制”;

希望你在理解它们的原理和用法后,能更准确地在项目中应用这两个工具,写出更加健壮、安全、高并发的 Java 程序。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐