Java 并发工具类实战:CountDownLatch 与 CyclicBarrier 深度解析
在 Java 并发编程中,除了
synchronized和Lock用于保证线程安全,还有一类 “并发工具类” 用于协调多个线程的执行顺序 ——CountDownLatch(倒计时门闩)和CyclicBarrier(循环屏障)就是其中的典型代表。它们能轻松实现 “线程等待多线程完成”“多线程等待彼此” 等复杂逻辑,广泛应用于测试、批量任务处理等场景。本文将从原理、区别、实战案例三个维度,详解这两个工具类的使用,帮你解决并发编程中的 “线程协调” 难题。
一、CountDownLatch:让线程等待多线程完成
CountDownLatch基于 “倒计时” 原理,核心功能是让一个或多个线程等待其他线程完成指定操作后,再继续执行。例如,主线程等待 10 个任务线程全部执行完毕后,再统计任务结果。
1.1 核心原理
- 初始化:创建
CountDownLatch时指定一个 “倒计时总数”(count),表示需要等待的线程 / 操作数量; - 倒计时:每个线程完成操作后,调用
countDown()方法,将count减 1; - 等待:需要等待的线程调用
await()方法,若count未减至 0,线程会阻塞;当count减至 0 时,所有阻塞的线程被唤醒,继续执行。
原理示意图:
plaintext
主线程 任务线程1 任务线程2 任务线程3
| | | |
| 创建CountDownLatch(3) | | |
|---------------------->| | |
| | 执行任务 | 执行任务 | 执行任务
| |------->countDown()(2) |
| | |------->countDown()(1)
| 调用await()阻塞 | | |
|<---------------------| | |
| | | |------->countDown()(0)
| 被唤醒,继续执行 | | |
|------->统计结果 | | |
1.2 核心方法
| 方法 | 作用 | 注意事项 |
|---|---|---|
CountDownLatch(int count) |
构造方法,初始化倒计时总数 | count必须大于 0,否则抛出IllegalArgumentException |
void countDown() |
倒计时减 1 | 若count已为 0,调用此方法无效果 |
void await() |
阻塞当前线程,直到count减至 0 |
可被InterruptedException中断(如其他线程调用interrupt()) |
boolean await(long timeout, TimeUnit unit) |
阻塞指定时间,若超时后count仍未为 0,返回false;否则返回true |
避免线程永久阻塞 |
1.3 实战场景 1:主线程等待多任务线程完成
需求:主线程创建 5 个任务线程,每个线程执行 “模拟数据处理” 操作(耗时 1 秒),主线程等待所有任务线程完成后,打印 “所有任务执行完毕”。
代码实现:
java
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class CountDownLatchDemo1 {
public static void main(String[] args) throws InterruptedException {
// 1. 初始化CountDownLatch,倒计时总数=5(5个任务线程)
CountDownLatch countDownLatch = new CountDownLatch(5);
// 2. 创建5个任务线程
for (int i = 1; i <= 5; i++) {
int taskId = i;
new Thread(() -> {
try {
System.out.println("任务线程" + taskId + "开始执行数据处理...");
// 模拟数据处理耗时(1秒)
TimeUnit.SECONDS.sleep(1);
System.out.println("任务线程" + taskId + "执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 3. 任务完成,倒计时减1(必须放在finally中,避免异常导致count未减)
countDownLatch.countDown();
}
}).start();
}
System.out.println("主线程等待所有任务线程完成...");
// 4. 主线程阻塞,直到count减至0
countDownLatch.await();
// 5. 所有任务线程完成,主线程继续执行
System.out.println("所有任务执行完毕,主线程开始汇总结果");
}
}
执行结果:
plaintext
主线程等待所有任务线程完成...
任务线程1开始执行数据处理...
任务线程2开始执行数据处理...
任务线程3开始执行数据处理...
任务线程4开始执行数据处理...
任务线程5开始执行数据处理...
任务线程1执行完毕
任务线程2执行完毕
任务线程3执行完毕
任务线程4执行完毕
任务线程5执行完毕
所有任务执行完毕,主线程开始汇总结果
关键注意点:
countDown()必须放在finally块中:若任务线程抛出异常,countDown()仍会执行,避免主线程永久阻塞;- 倒计时总数与任务数一致:确保每个任务线程都调用一次
countDown(),否则count无法减至 0,主线程会一直阻塞(可使用await(timeout)设置超时时间)。
1.4 实战场景 2:多线程等待初始化完成
需求:系统启动时,需要先完成 “配置加载”“数据库连接初始化”“缓存加载” 3 个初始化操作(每个操作由一个线程执行),待所有初始化完成后,10 个业务线程才能开始处理业务。
代码实现:
java
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class CountDownLatchDemo2 {
// 初始化倒计时器:需要等待3个初始化操作
private static final CountDownLatch initLatch = new CountDownLatch(3);
public static void main(String[] args) {
// 1. 启动3个初始化线程
startInitThreads();
// 2. 启动10个业务线程(业务线程会等待初始化完成)
for (int i = 1; i <= 10; i++) {
int threadId = i;
new Thread(() -> {
try {
System.out.println("业务线程" + threadId + "等待系统初始化...");
// 等待初始化完成(count减至0)
initLatch.await();
// 初始化完成,开始处理业务
System.out.println("业务线程" + threadId + "开始处理业务");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
// 启动3个初始化线程:配置加载、数据库初始化、缓存加载
private static void startInitThreads() {
// 初始化1:配置加载
new Thread(() -> {
try {
System.out.println("初始化线程-配置加载:开始加载系统配置...");
TimeUnit.SECONDS.sleep(2); // 模拟耗时
System.out.println("初始化线程-配置加载:配置加载完成");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
initLatch.countDown();
}
}).start();
// 初始化2:数据库连接初始化
new Thread(() -> {
try {
System.out.println("初始化线程-数据库:开始初始化数据库连接...");
TimeUnit.SECONDS.sleep(3); // 模拟耗时
System.out.println("初始化线程-数据库:数据库连接初始化完成");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
initLatch.countDown();
}
}).start();
// 初始化3:缓存加载
new Thread(() -> {
try {
System.out.println("初始化线程-缓存:开始加载缓存数据...");
TimeUnit.SECONDS.sleep(1); // 模拟耗时
System.out.println("初始化线程-缓存:缓存加载完成");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
initLatch.countDown();
}
}).start();
}
}
执行结果(部分):
plaintext
初始化线程-配置加载:开始加载系统配置...
初始化线程-数据库:开始初始化数据库连接...
初始化线程-缓存:开始加载缓存数据...
业务线程1等待系统初始化...
业务线程2等待系统初始化...
...(省略其他业务线程)
初始化线程-缓存:缓存加载完成
初始化线程-配置加载:配置加载完成
初始化线程-数据库:数据库连接初始化完成
业务线程1开始处理业务
业务线程2开始处理业务
...(所有业务线程开始执行)
核心价值:
- 实现 “一次性初始化,多线程复用”:所有业务线程无需各自等待初始化,只需等待一次
initLatch的count减至 0; - 解耦初始化与业务逻辑:初始化线程和业务线程通过
CountDownLatch通信,无需硬编码依赖。
二、CyclicBarrier:让多线程互相等待
CyclicBarrier基于 “屏障” 原理,核心功能是让多个线程互相等待,直到所有线程都到达指定 “屏障点” 后,再一起继续执行。例如,3 个线程共同执行一个任务,需先各自完成 “数据采集”,待所有线程都采集完数据后,再一起执行 “数据汇总”。
2.1 核心原理
- 初始化:创建
CyclicBarrier时指定 “参与线程数”(parties),表示需要等待的线程数量; - 到达屏障:每个线程执行到屏障点时,调用
await()方法,线程会阻塞,直到所有parties个线程都调用await(); - 屏障突破:当最后一个线程调用
await()后,所有阻塞的线程被唤醒,一起继续执行;若设置了 “屏障动作”(Runnable),会先执行屏障动作,再唤醒线程。
原理示意图:
plaintext
线程A 线程B 线程C
| | |
| 执行任务阶段1 | 执行任务阶段1 | 执行任务阶段1
|------->到达屏障点 |------->到达屏障点 |
| 调用await()阻塞 | 调用await()阻塞 |
|<------------------|------------------|
| | |------->到达屏障点
| | | 调用await()
| 屏障动作执行(可选)| |
|------->所有线程被唤醒 |
| 执行任务阶段2 | 执行任务阶段2 | 执行任务阶段2
2.2 核心方法
| 方法 | 作用 | 注意事项 |
|---|---|---|
CyclicBarrier(int parties) |
构造方法,初始化参与线程数 | parties必须大于 0 |
CyclicBarrier(int parties, Runnable barrierAction) |
构造方法,指定参与线程数和屏障动作 | 屏障动作由最后一个到达屏障的线程执行 |
int await() |
到达屏障,阻塞线程直到所有线程到达 | 可被InterruptedException中断;若其他线程被中断,当前线程会抛出BrokenBarrierException |
int await(long timeout, TimeUnit unit) |
阻塞指定时间,超时后抛出TimeoutException |
避免线程永久阻塞 |
void reset() |
重置屏障为初始状态 | 若有线程已阻塞在屏障点,会抛出BrokenBarrierException |
2.3 核心特性:可循环使用
与CountDownLatch(倒计时一旦为 0,无法重置)不同,CyclicBarrier支持循环使用—— 当所有线程突破一次屏障后,屏障会自动重置,可再次用于下一轮线程等待。例如,3 个线程可重复执行 “阶段 1→屏障→阶段 2” 的流程。
2.4 实战场景 1:多线程分阶段执行任务
需求:4 个线程共同处理一个 “数据统计” 任务,分两个阶段执行:
- 阶段 1:每个线程采集各自负责的数据源数据(耗时 1-3 秒);
- 阶段 2:所有线程采集完数据后,一起汇总数据(需先执行 “屏障动作”:打印 “所有线程已完成数据采集”)。
代码实现:
java
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;
public class CyclicBarrierDemo1 {
public static void main(String[] args) {
// 1. 初始化CyclicBarrier:参与线程数=4,屏障动作(最后一个线程到达后执行)
CyclicBarrier cyclicBarrier = new CyclicBarrier(4, () -> {
System.out.println("=== 屏障动作执行:所有线程已完成数据采集,准备进入汇总阶段 ===");
});
// 2. 创建4个线程,每个线程负责一个数据源
for (int i = 1; i <= 4; i++) {
int dataSourceId = i;
new Thread(() -> {
try {
// 阶段1:采集数据
System.out.println("线程" + Thread.currentThread().getId() + "(数据源" + dataSourceId + ")开始采集数据...");
// 模拟采集耗时(1-3秒随机)
TimeUnit.SECONDS.sleep((long) (Math.random() * 2 + 1));
System.out.println("线程" + Thread.currentThread().getId() + "(数据源" + dataSourceId + ")数据采集完成,到达屏障点");
// 到达屏障,等待其他线程
cyclicBarrier.await();
// 阶段2:汇总数据(所有线程被唤醒后执行)
System.out.println("线程" + Thread.currentThread().getId() + "(数据源" + dataSourceId + ")开始汇总数据");
TimeUnit.SECONDS.sleep(1); // 模拟汇总耗时
System.out.println("线程" + Thread.currentThread().getId() + "(数据源" + dataSourceId + ")数据汇总完成");
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}
执行结果(部分):
plaintext
线程11(数据源1)开始采集数据...
线程12(数据源2)开始采集数据...
线程13(数据源3)开始采集数据...
线程14(数据源4)开始采集数据...
线程12(数据源2)数据采集完成,到达屏障点
线程11(数据源1)数据采集完成,到达屏障点
线程14(数据源4)数据采集完成,到达屏障点
线程13(数据源3)数据采集完成,到达屏障点
=== 屏障动作执行:所有线程已完成数据采集,准备进入汇总阶段 ===
线程13(数据源3)开始汇总数据
线程14(数据源4)开始汇总数据
线程11(数据源1)开始汇总数据
线程12(数据源2)开始汇总数据
线程13(数据源3)数据汇总完成
线程14(数据源4)数据汇总完成
线程11(数据源1)数据汇总完成
线程12(数据源2)数据汇总完成
关键观察点:
- 屏障动作由最后一个到达屏障的线程执行(如线程 13);
- 所有线程在屏障动作执行后,才一起进入阶段 2,实现 “分阶段同步”。
2.5 实战场景 2:循环执行多轮任务
需求:2 个线程模拟 “运动员训练”,需完成 3 轮训练,每轮训练流程为:
- 各自完成 “热身”(耗时 1 秒);
- 所有线程热身完成后,一起执行 “高强度训练”(屏障动作);
- 重复 3 轮。
代码实现:
java
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;
public class CyclicBarrierDemo2 {
public static void main(String[] args) {
// 初始化CyclicBarrier:参与线程数=2,屏障动作(每轮热身完成后执行)
CyclicBarrier cyclicBarrier = new CyclicBarrier(2, () -> {
System.out.println("=== 所有运动员热身完成,开始高强度训练 ===");
try {
TimeUnit.SECONDS.sleep(2); // 模拟高强度训练耗时
System.out.println("=== 本轮高强度训练结束,休息1秒准备下一轮 ===");
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 定义训练线程(2个运动员)
Runnable athleteTask = () -> {
try {
// 完成3轮训练
for (int round = 1; round <= 3; round++) {
System.out.println("运动员" + Thread.currentThread().getId() + ":第" + round + "轮热身开始");
TimeUnit.SECONDS.sleep(1); // 模拟热身耗时
System.out.println("运动员" + Thread.currentThread().getId() + ":第" + round + "轮热身完成,等待队友");
// 到达屏障,等待队友(每轮都会等待)
cyclicBarrier.await();
}
System.out.println("运动员" + Thread.currentThread().getId() + ":所有训练轮次完成!");
} catch (Exception e) {
e.printStackTrace();
}
};
// 启动2个运动员线程
new Thread(athleteTask).start();
new Thread(athleteTask).start();
}
}
执行结果(部分):
plaintext
运动员11:第1轮热身开始
运动员12:第1轮热身开始
运动员11:第1轮热身完成,等待队友
运动员12:第1轮热身完成,等待队友
=== 所有运动员热身完成,开始高强度训练 ===
=== 本轮高强度训练结束,休息1秒准备下一轮 ===
运动员12:第2轮热身开始
运动员11:第2轮热身开始
运动员12:第2轮热身完成,等待队友
运动员11:第2轮热身完成,等待队友
=== 所有运动员热身完成,开始高强度训练 ===
=== 本轮高强度训练结束,休息1秒准备下一轮 ===
...(省略第3轮)
运动员11:所有训练轮次完成!
运动员12:所有训练轮次完成!
核心价值:
- 循环复用屏障:无需创建多个
CyclicBarrier,一个实例即可支持多轮线程同步; - 简化多轮任务逻辑:每轮任务的同步逻辑统一,无需手动管理轮次状态。
三、CountDownLatch 与 CyclicBarrier 的核心区别
很多开发者会混淆CountDownLatch和CyclicBarrier,但两者的设计目标和使用场景差异显著,核心区别如下:
| 对比维度 | CountDownLatch | CyclicBarrier |
|---|---|---|
| 核心功能 | 一个 / 多个线程等待其他线程完成 | 多个线程互相等待,一起突破屏障 |
| 复用性 | 不可复用(count 减至 0 后,无法重置) | 可循环复用(突破屏障后自动重置) |
| 参与角色 | 分 “等待方” 和 “被等待方”(角色不同) | 所有线程角色相同(均为 “等待方”) |
| 屏障动作 | 无(需额外线程实现类似逻辑) | 支持屏障动作(最后一个线程到达后执行) |
| 异常处理 | 被等待方抛出异常,需确保countDown()执行(放在 finally) |
某线程被中断,会导致其他线程抛出BrokenBarrierException |
| 典型场景 | 主线程等待多任务完成、多线程等待初始化 | 多线程分阶段执行、循环任务同步 |
记忆口诀:
- 「Latch 倒计时,一次用完就失效;Barrier 循环等,线程平等同前行」
四、常见问题与最佳实践
4.1 常见问题
- CountDownLatch 的 count 未减至 0 导致线程永久阻塞:解决方法:使用
await(long timeout, TimeUnit unit)设置超时时间,或在finally块中调用countDown(); - CyclicBarrier 的线程被中断导致屏障损坏:解决方法:捕获
BrokenBarrierException,调用cyclicBarrier.reset()重置屏障,重新执行任务; - 线程数量与 parties/count 不匹配:例如,
CyclicBarrier(3)但只有 2 个线程调用await(),会导致线程永久阻塞。解决方法:确保参与线程数与初始化时的数量一致。
4.2 最佳实践
- 明确使用场景:
- 若需 “单向等待”(如主线程等任务线程),用
CountDownLatch; - 若需 “双向等待”(如多线程分阶段执行),用
CyclicBarrier;
- 若需 “单向等待”(如主线程等任务线程),用
- 设置超时时间:无论是
CountDownLatch.await()还是CyclicBarrier.await(),都建议设置超时时间,避免线程因异常永久阻塞; - 优先使用线程池:实际开发中,避免手动创建线程,应使用
ThreadPoolExecutor管理线程,结合并发工具类使用,提升资源利用率; - 日志记录关键节点:在
countDown()、await()前后打印日志,方便问题排查(如定位哪个线程未调用countDown())。
五、总结
CountDownLatch和CyclicBarrier是 Java 并发编程中 “线程协调” 的利器,它们分别解决了 “单向等待” 和 “双向等待” 的问题,避免了复杂的wait()/notify()逻辑(易出错且效率低)。
- CountDownLatch:适合 “一个或多个线程等待其他线程完成” 的场景,如任务汇总、系统初始化;
- CyclicBarrier:适合 “多个线程分阶段同步执行” 的场景,如多轮训练、分阶段数据处理。
在实际开发中,需根据线程角色和任务流程选择合适的工具类,并注意异常处理和资源释放,才能写出高效、安全的并发代码。
更多推荐

所有评论(0)