
Java并发工具包中CountDownLatch与CyclicBarrier使用对比:从原理到实战的深度解析
在Java并发编程的实战中,我们常常会遇到需要协调多个线程步调的场景。比如,主线程需要等待所有子线程完成初始化后才能开始工作,或者一组线程必须都到达某个“集合点”才能继续下一阶段的任务。JUC(java.util.concurrent)包为我们提供了两个强大的同步辅助类来解决这类问题:CountDownLatch和CyclicBarrier。很多开发者初看觉得它们功能相似,容易混淆。今天,我就结合自己的踩坑经验,带大家深入剖析两者的核心区别、适用场景以及实战中的正确用法。
一、核心概念与原理:它们到底在做什么?
首先,我们必须从设计意图上理解它们的根本不同。
CountDownLatch(倒计时闩锁):这是一个“一次性”的同步工具。你可以把它想象成一个带有数字计数器的门闩。构造时设定一个初始计数值(比如N)。任何线程(通常是主线程)调用await()方法时,会被阻塞,直到其他线程调用countDown()方法将计数器减到0,门闩打开,所有等待的线程才能继续执行。它的核心是“一个或多个线程等待一组事件发生”。事件由countDown()触发,等待由await()执行。计数器无法重置,用完即废。
CyclicBarrier(循环屏障):这是一个“可循环使用”的同步工具。它更像一个多人会议的集合点。构造时设定一个参与线程数(比如N)。每个线程执行到屏障点(调用await())时会被阻塞,并告知屏障“我已到达”。当第N个线程到达后,屏障被“冲破”(trip),所有被阻塞的线程被同时释放,并且屏障的计数器会自动重置,可以迎接下一轮(Cyclic)的同步。它的核心是“一组线程相互等待,直到所有线程都到达某个公共屏障点”。此外,它还可以在冲破屏障时,执行一个可选的“屏障动作”(Runnable)。
二、实战代码示例:一看就懂的用法
理论说再多不如代码来得直观。我们来看两个典型的场景。
场景1:使用CountDownLatch实现主线程等待所有子线程准备完毕
模拟一个游戏服务启动场景:需要先加载地图、连接数据库、初始化网络,所有准备工作完成后,才能通知“游戏开始”。
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 初始化计数器为3,代表3项准备工作
CountDownLatch latch = new CountDownLatch(3);
new Thread(new Worker("加载地图", 2, latch)).start();
new Thread(new Worker("连接数据库", 1, latch)).start();
new Thread(new Worker("初始化网络", 3, latch)).start();
System.out.println("主线程:等待所有准备工作完成...");
// 主线程在此等待,直到计数器归零
latch.await();
System.out.println("主线程:所有准备就绪,游戏开始!");
}
static class Worker implements Runnable {
private final String taskName;
private final int workTime;
private final CountDownLatch latch;
Worker(String taskName, int workTime, CountDownLatch latch) {
this.taskName = taskName;
this.workTime = workTime;
this.latch = latch;
}
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " 开始:" + taskName);
TimeUnit.SECONDS.sleep(workTime); // 模拟耗时工作
System.out.println(Thread.currentThread().getName() + " 完成:" + taskName);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 关键!工作完成后,计数器减1
latch.countDown();
}
}
}
}
踩坑提示:务必在finally块中调用countDown(),确保即使线程执行异常,计数器也能递减,避免主线程永远等待。
场景2:使用CyclicBarrier实现多线程分阶段计算
模拟一个分布式计算场景:多个工作线程分别计算一部分数据,所有线程计算完第一阶段后,汇总中间结果,然后同时开始第二阶段计算。
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
public static void main(String[] args) {
// 参与线程数为3,所有线程到达屏障后,执行屏障动作(由最后一个到达的线程执行)
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("=== 所有线程已完成第一阶段,开始第二阶段 ===");
});
for (int i = 0; i < 3; i++) {
new Thread(new ComputeWorker(i, barrier)).start();
}
// 注意:CyclicBarrier可以重复使用,这里为了演示只进行了一轮。
}
static class ComputeWorker implements Runnable {
private final int id;
private final CyclicBarrier barrier;
ComputeWorker(int id, CyclicBarrier barrier) {
this.id = id;
this.barrier = barrier;
}
@Override
public void run() {
try {
System.out.println("Worker " + id + " 开始第一阶段计算");
Thread.sleep((long) (Math.random() * 2000)); // 模拟计算耗时
System.out.println("Worker " + id + " 完成第一阶段,等待其他线程");
// 到达屏障点,等待其他线程
barrier.await();
// 屏障冲破后,所有线程同时开始第二阶段
System.out.println("Worker " + id + " 开始第二阶段计算");
Thread.sleep((long) (Math.random() * 1000));
System.out.println("Worker " + id + " 完成所有工作");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}
}
实战经验:CyclicBarrier的await()方法会抛出BrokenBarrierException。如果一个等待的线程被中断、超时或者屏障被重置,其他等待的线程就会收到此异常,表示屏障被“破坏”了。这在处理复杂错误时很重要。
三、核心区别与选型指南
理解了用法,我们来系统对比一下,这能帮你做出正确的技术选型。
1. 核心角色与等待对象不同:
- CountDownLatch:通常由一个或多个线程(等待者)等待一组事件(countDown调用)发生。事件和等待的线程是解耦的。
- CyclicBarrier:是一组线程(参与者自身)相互等待,直到所有线程都到达屏障点。线程既是事件的触发者,也是等待者。
2. 可重用性不同:
- CountDownLatch:一次性。计数器归零后,所有
await调用会立即通过,但门闩状态无法重置。想再用,只能新建一个对象。 - CyclicBarrier:可循环使用。当屏障被冲破后,计数器会自动重置,可以立即用于下一轮的线程同步。这是“Cyclic”的含义。
3. 功能扩展性不同:
- CountDownLatch:功能纯粹,就是计数和等待。
- CyclicBarrier:功能更丰富。它允许指定一个屏障动作(Runnable),当所有线程到达后,在释放它们之前,由最后一个到达的线程执行此动作,非常适合用于合并各线程的阶段性结果。
四、总结与选型口诀
经过上面的分析和实战,我们可以得出一个简单的选型口诀:
- 用 CountDownLatch 当:你的场景是“一等多”或“多等多”(一个/多个线程等待多个事件完成),且这个同步过程只需要一次。典型场景:服务启动等待依赖组件初始化、测试中并发执行用例后统计结果。
- 用 CyclicBarrier 当:你的场景是“多等多”(一组线程相互等待),并且这个同步过程需要重复发生(分阶段、多轮次)。典型场景:并行迭代计算(如遗传算法)、多线程测试(模拟并发用户同时发起请求)。
最后,再分享一个我踩过的坑:在Web服务器中,我曾用CountDownLatch来等待所有处理线程结束以优雅关机。但如果某个线程因异常未能调用countDown(),就会导致关机逻辑永远挂起。因此,异常处理和资源清理的严谨性,在使用这两个工具时至关重要。希望这篇对比能帮助你在并发编程的道路上,更加得心应手。

评论(0)