在Java并发编程中,CountDownLatch
和 CyclicBarrier
是两个非常重要的同步工具类,它们都用于协调多个线程之间的执行顺序。虽然它们的功能有些相似,但设计目的和使用场景有显著区别。
核心目标: 两者都用于协调多个线程的执行顺序,使某些线程等待其他线程完成特定操作后再继续执行。它们解决的是线程间“汇合点
”的问题。
📌 一、CountDownLatch (倒计时闩锁)
✅ 核心概念
- 一个一次性使用的同步辅助工具。
- 它允许一个或多个线程等待,直到在其他线程中执行的一组操作完成。
- 内部维护一个计数器 (count),该计数器在初始化时设定。线程通过调用
countDown()
方法递减计数器。等待的线程调用await()
方法会被阻塞,直到计数器减到零。
🔁 特点
- 不可重用:一旦计数器减为0,就不能再复位。
- 适合“一个线程等待多个线程完成”的场景。
- 可以看作是一个一次性的屏障。
✅ 关键方法
CountDownLatch(int count)
: 构造函数,指定初始计数值。void await()
: 使当前线程等待,直到计数器减到零(除非线程被中断)。boolean await(long timeout, TimeUnit unit)
: 使当前线程等待,直到计数器减到零、或超过指定等待时间、或线程被中断。void countDown()
: 将计数器减1。如果计数器达到零,则释放所有等待的线程。long getCount()
: 返回当前计数值(主要用于调试和测试)。
✅ 工作原理
- 主线程创建
CountDownLatch
对象,指定初始计数值N
(代表需要等待完成的任务数量)。 - 主线程启动
N
个工作线程(或任务),并将CountDownLatch
对象传递给它们。 - 每个工作线程在完成其任务后,调用
latch.countDown()
。 - 主线程(或其他需要等待的线程)在启动所有工作线程后,调用
latch.await()
。此时它会阻塞。 - 当
N
个工作线程都调用了countDown()
,计数器减到 0。 - 主线程(等待线程)从
await()
返回,继续执行后续逻辑。
✅ 典型应用场景
- 启动服务前的依赖检查: 确保所有必需的服务(数据库连接、缓存加载、配置文件读取等)都初始化完成后再启动主应用。
- 并行任务汇总: 将一个大任务拆分成多个子任务并行执行,主线程等待所有子任务完成后再进行结果汇总。
- 模拟并发测试: 让多个测试线程同时开始执行某个操作(所有测试线程都
await()
在同一个CountDownLatch
上,主线程countDown()
一次释放所有等待线程)。 - 游戏开始等待: 等待所有玩家准备就绪(每个玩家准备完成调用
countDown()
)后才开始游戏。
🧪 示例
主线程等待多个子线程完成
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
int threadCount = 3;
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 1; i <= threadCount; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 正在执行...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 执行完毕");
latch.countDown(); // 计数器减一
}, "线程-" + i).start();
}
System.out.println("主线程等待所有子线程完成...");
latch.await(); // 阻塞直到计数器为0
System.out.println("所有子线程已完成,主线程继续执行");
}
}
火箭发射前检查
public class RocketLaunch {
public static void main(String[] args) throws InterruptedException {
// 定义需要等待的检查项数量
int checkItems = 4; // 例如:燃料、引擎、导航、通信
CountDownLatch preLaunchCheckLatch = new CountDownLatch(checkItems);
// 创建并启动检查线程
ExecutorService executor = Executors.newFixedThreadPool(checkItems);
executor.execute(new CheckTask("燃料系统检查", 2000, preLaunchCheckLatch));
executor.execute(new CheckTask("引擎系统检查", 1500, preLaunchCheckLatch));
executor.execute(new CheckTask("导航系统检查", 3000, preLaunchCheckLatch));
executor.execute(new CheckTask("通信系统检查", 1000, preLaunchCheckLatch));
// 主线程(指挥中心)等待所有检查完成
System.out.println("指挥中心:等待所有发射前检查完成...");
preLaunchCheckLatch.await(); // 阻塞直到计数器为0
System.out.println("指挥中心:所有检查通过!开始倒计时发射!");
executor.shutdown(); // 关闭线程池
}
static class CheckTask implements Runnable {
private final String taskName;
private final int duration;
private final CountDownLatch latch;
CheckTask(String taskName, int duration, CountDownLatch latch) {
this.taskName = taskName;
this.duration = duration;
this.latch = latch;
}
@Override
public void run() {
try {
System.out.println(taskName + " 开始...");
Thread.sleep(duration); // 模拟检查耗时
System.out.println(taskName + " 完成!");
latch.countDown(); // 检查完成,计数器减1
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
📌 二、CyclicBarrier (循环栅栏)
✅核心概念
- 一个可以重复使用的同步辅助工具。
- 它允许一组线程相互等待,直到所有线程都到达一个公共的屏障点,然后它们可以一起继续执行。
- 它支持一个可选的
Runnable
动作,在所有线程到达屏障点后,由最后一个到达的线程去执行这个屏障动作(barrier action)。 - 内部也维护一个计数器 (count),表示需要到达屏障的线程数。线程通过调用
await()
表示自己已到达屏障点,并等待其他线程。
🔁 特点
- 可以重复使用:每次所有线程到达屏障后,计数器会自动重置。
- 适合“多线程相互等待”的场景。
- 支持 barrier action。
- 如果某个线程中断或者超时,可能会导致整个屏障失败(抛出异常)。
✅关键方法
CyclicBarrier(int parties)
: 构造函数,指定需要相互等待的线程数量。CyclicBarrier(int parties, Runnable barrierAction)
: 构造函数,指定线程数和当所有线程到达屏障后要执行的屏障动作(由最后一个到达屏障的线程执行)。int await()
: 使当前线程等待,直到所有线程都调用了此方法(除非线程被中断或屏障被重置/破坏)。返回当前线程的到达索引。int await(long timeout, TimeUnit unit)
: 带超时的等待。void reset()
: 将屏障重置为其初始状态。如果任何线程正在屏障处等待,它们将抛出BrokenBarrierException
。用于处理一个线程失败的情况。
✅工作原理
- 创建
CyclicBarrier
对象,指定参与线程数N
和可选的barrierAction
。 N
个线程各自执行自己的任务。- 当每个线程完成自己阶段性的任务,需要等待其他线程时,调用
barrier.await()
。 - 调用
await()
的线程会被阻塞。 - 当第
N
个线程调用await()
时:- 如果定义了
barrierAction
,则第N
个线程会执行它。 - 然后,所有
N
个被阻塞的线程会被同时唤醒释放,继续执行各自后续的任务。 - 屏障自动重置为初始状态,可以再次使用(这就是“循环”的含义)。
- 如果定义了
- 如果等待过程中有线程被中断、超时、或者调用
reset()
,屏障会被置为破坏 (broken) 状态,所有等待的线程(包括后续调用await()
的线程)将抛出BrokenBarrierException
。
✅典型应用场景
- 分阶段并行计算: 将计算分成多个阶段,每个阶段所有线程完成自己的工作后,在屏障处同步,确保所有线程都完成当前阶段后再一起进入下一阶段。例如,并行矩阵乘法、迭代计算。
- 多线程数据加载与处理: 多个线程分别加载数据的一部分,全部加载完成后(在屏障处汇合),再各自或一起开始处理数据。
- 模拟多次并发测试: 需要重复进行多轮并发测试的场景,每轮测试所有线程在开始点同步(
await()
),然后同时开始执行测试任务。屏障可重复使用。
🧪 示例
多个线程相互等待,每轮完成后重置
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
public static void main(String[] args) {
int threadCount = 3;
CyclicBarrier barrier = new CyclicBarrier(threadCount, () -> {
System.out.println("【所有线程已到达屏障点,开始新一轮】");
});
for (int i = 1; i <= threadCount; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " 准备中...");
Thread.sleep((long)(Math.random() * 2000));
System.out.println(Thread.currentThread().getName() + " 到达屏障点");
barrier.await(); // 等待其他线程
System.out.println(Thread.currentThread().getName() + " 继续执行");
Thread.sleep((long)(Math.random() * 2000));
System.out.println(Thread.currentThread().getName() + " 再次准备...");
Thread.sleep((long)(Math.random() * 2000));
System.out.println(Thread.currentThread().getName() + " 第二次到达屏障点");
barrier.await();
System.out.println(Thread.currentThread().getName() + " 最终执行完毕");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}, "线程-" + i).start();
}
}
}
多线程计算报表汇总
public class FinancialReport {
public static void main(String[] args) {
// 定义参与计算的线程数(部门数)和汇总动作
int departments = 3;
Runnable summaryAction = () -> System.out.println("\n所有部门数据已就绪,开始生成季度汇总报告...\n");
CyclicBarrier reportBarrier = new CyclicBarrier(departments, summaryAction);
// 创建并启动部门计算线程
ExecutorService executor = Executors.newFixedThreadPool(departments);
executor.execute(new DepartmentTask("销售部", 1800, reportBarrier));
executor.execute(new DepartmentTask("市场部", 2200, reportBarrier));
executor.execute(new DepartmentTask("研发部", 3000, reportBarrier));
executor.shutdown(); // 不再接受新任务,等待已提交任务完成
}
static class DepartmentTask implements Runnable {
private final String deptName;
private final int calcTime;
private final CyclicBarrier barrier;
DepartmentTask(String deptName, int calcTime, CyclicBarrier barrier) {
this.deptName = deptName;
this.calcTime = calcTime;
this.barrier = barrier;
}
@Override
public void run() {
try {
// 阶段1: 计算本部门数据
System.out.println(deptName + " 正在计算本季度数据...");
Thread.sleep(calcTime); // 模拟计算耗时
System.out.println(deptName + " 数据计算完成!等待其他部门...");
barrier.await(); // 等待所有部门计算完成
// 阶段2: 所有部门数据就绪后,屏障动作(汇总报告)由最后一个到达的线程执行
// 然后所有线程同时继续执行这里(如果有后续任务)
// 例如:每个线程可以开始基于汇总数据进行本地分析(如果需要)
System.out.println(deptName + " 收到汇总报告,开始进行本地分析...");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
}
}
📌 三、CountDownLatch vs CyclicBarrier(核心区别)
特性 | CountDownLatch | CyclicBarrier |
---|---|---|
使用次数 | 一次性:计数器减到0后失效,不能重置。 | 可循环使用:计数器归零后自动重置,可再次使用。 |
参与者角色 | 两类角色: 1. 递减者 ( countDown() )2. 等待者 ( await() )。通常递减者和等待者是不同的线程组(如工作线程 vs 主线程)。 | 单一角色:所有线程都是参与者,都调用 await() 表示到达屏障点并相互等待。 |
计数器管理 | 只能由工作线程递减 (countDown() )。主线程不能增加。 | 由参与者线程调用 await() 隐式递减。 |
等待行为 | 等待者 (await() ) 等待计数器归零。 | 所有参与者线程相互等待,直到所有都调用 await() 。 |
屏障动作 | 无内置屏障动作。 | 支持可选的 Runnable barrierAction ,当所有线程到达屏障时,由最后一个到达的线程执行。 |
重置 | 无法重置。 | 提供 reset() 方法手动重置屏障(会导致等待线程抛出异常)。 |
异常处理 | 相对简单。countDown() 失败可能导致主线程永远等待。 | 更复杂。一个线程中断/超时/异常会导致屏障破坏 (BrokenBarrierException ),影响所有等待线程。需要 reset() 恢复。 |
依赖方向 | 等待者依赖于递减者完成任务。 | 参与者线程相互依赖,共同到达汇合点。 |
典型场景关键词 | 主从依赖、启动准备、一次性汇总 | 分阶段协同、并行计算同步点、重复测试 |
📌 四、应用场景对比
✅ CountDownLatch 场景
- 主线程等待多个子线程加载配置完成后再启动服务。
- 并发测试中模拟多个请求同时发起。
- 各个模块并行加载数据,主模块等待全部加载完成后汇总。
✅ CyclicBarrier 场景
- 多个玩家同时准备游戏,每轮结束后重新开始。
- 分布式系统中多个节点协同计算,每一轮计算完成后汇总结果。
- 模拟比赛中的接力赛跑,每个选手必须等前一个完成才能开始。
📌 五、建议
使用场景 | 推荐类 |
---|---|
一个线程等待多个线程完成 | CountDownLatch |
多个线程相互等待,多次使用 | CyclicBarrier |
需要 barrier action | CyclicBarrier |
一次性同步点 | CountDownLatch |
多轮同步点 | CyclicBarrier |
📌 六、常见问题
Q: CountDownLatch 和 join 的区别?
join()
是线程级别的等待,需要显式地对每个线程调用;CountDownLatch
更灵活,可以控制多个线程完成事件,不依赖具体线程对象。
Q: 如何选择 CountDownLatch 还是 CyclicBarrier?
- 如果是一次性同步点 → 用
CountDownLatch
; - 如果是多轮同步点且需要协作 → 用
CyclicBarrier
。
Q: CyclicBarrier 是否线程安全?
- 是的,它是线程安全的,内部使用了 ReentrantLock 和 Condition 来实现同步。