CountDownLatch和CyclicBarrier
1. CountDownLatch和CyclicBarrier:让多线程步调一致
1.1. 应用背景
应用背景:对账系统优化
- 系统逻辑:
-
- 用户下单 → 订单库;
- 物流发货 → 派送单库;
- 对账系统每日校验是否存在异常订单。
- 原始代码流程:
-
- 单线程串行执行:查订单 → 查派送单 → 对账 → 写入差异库;
- 性能瓶颈:
getPOrders()
和getDOrders()
查询操作耗时。
1.2. 优化方案
1. 第一阶段优化:多线程并行查询
改进点:
getPOrders()
和getDOrders()
可并行,无依赖顺序。
实现方式 :手动创建线程 + join()
Thread T1 = new Thread(() -> { pos = getPOrders(); });
Thread T2 = new Thread(() -> { dos = getDOrders(); });
T1.start(); T2.start();
T1.join(); T2.join(); // 主线程等待
- 优点:能有效利用 CPU,提高查询并发效率。
- 缺点:每次循环都创建线程,开销大,不可复用。
2. 第二阶段优化:线程池 + CountDownLatch
问题:
- 使用线程池复用线程后,不能再用
join()
等待线程结束。
解决方案:CountDownLatch
- 功能:一个线程等待其他多个线程完成操作。
- 用法:
CountDownLatch latch = new CountDownLatch(2);executor.execute(() -> { pos = getPOrders(); latch.countDown(); });
executor.execute(() -> { dos = getDOrders(); latch.countDown(); });latch.await(); // 等两个查询完成
diff = check(pos, dos); save(diff);
- 优点:线程可复用,性能更优。
- 特性:一次性使用,计数器不能重置。
3. 第三阶段优化:完全并行处理
目标:
- 查询操作与对账操作并行:生产者-消费者模型
技术设计:
- 两个线程 T1/T2 分别作为“生产者”,放入两个队列
pos
/dos
; - 一个线程 T3 作为“消费者”,从两个队列中取出一对数据,进行对账。
步调一致 + 回调触发:使用 CyclicBarrier
问题:
- 如何保证 T1 和 T2 查询步调一致?
- 如何在它们都完成后触发对账操作(T3)?
解决方案:CyclicBarrier
- 功能:一组线程互相等待,计数器归零自动重置,并可执行回调函数。
- 用法示例:
final CyclicBarrier barrier = new CyclicBarrier(2, () -> {executor.execute(() -> check()); // 回调:执行对账
});Thread T1 = new Thread(() -> {while(存在未对账订单){pos.add(getPOrders());barrier.await(); // 等待 T2}
});
Thread T2 = new Thread(() -> {while(存在未对账订单){dos.add(getDOrders());barrier.await(); // 等待 T1}
});
- 对账逻辑:
void check() {P p = pos.remove(0);D d = dos.remove(0);diff = check(p, d);save(diff);
}
CyclicBarrier 特点:
特性 | 描述 |
可重用 | 自动重置计数器,适合循环场景 |
回调机制 | 当所有线程到达屏障点时,自动触发操作 |
用途 | 多线程“并肩作战”场景,例如同步起跑 |
CountDownLatch vs. CyclicBarrier 对比总结:
特性 | CountDownLatch | CyclicBarrier |
作用场景 | 一个线程等待多个线程 | 多个线程互相等待 |
是否可重用 | ❌ 不可重用 | ✅ 可重用 |
回调机制 | ❌ 无回调 | ✅ 有回调 |
类比场景 | 旅游团团长等所有人到齐 | 驴友一起出发不离不弃 |
小结与实践建议
- 如果是“一人等多人”:选
CountDownLatch
; - 如果是“多人互等”:选
CyclicBarrier
; - 线程池 + 并行执行 + 正确同步等待 → 提高系统吞吐;
- 实际项目中尽量使用 JDK 提供的并发工具类,不建议自行实现同步逻辑。+