引言:为什么多线程协作需要“协调员”?
在Java多线程开发中,我们经常遇到这样的场景:
- 主线程需要等待所有子线程完成数据加载后,才能汇总结果;
- 多个子线程需要“齐步走”,例如游戏中所有玩家准备完成后,才能开始游戏;
- 多阶段任务处理(如先计算,再汇总,再输出),每一步都需要所有线程完成当前阶段才能进入下一阶段。
传统的synchronized
或wait/notify
虽然能实现同步,但代码复杂且容易出错。Java并发包(java.util.concurrent
)提供的CountDownLatch
和CyclicBarrier
,正是解决这类问题的“协作利器”。本文将从原理到实战,带你彻底掌握这两个工具类。
一、CountDownLatch:倒计时门闩,等待任务完成
1.1 核心概念与原理
CountDownLatch
(倒计时门闩)通过一个计数器实现线程同步。初始化时设置计数器值(如count=5
),当任意线程完成任务后调用countDown()
(计数器减1),其他线程通过await()
等待计数器归零。
核心方法:
CountDownLatch(int count)
:构造函数,初始化计数器;void countDown()
:计数器减1(通常由子线程调用);boolean await(long timeout, TimeUnit unit)
:等待计数器归零(可设置超时);void await()
:阻塞等待计数器归零(无超时)。
1.2 实战示例:主线程等待所有子线程完成
场景:一个电商系统需要从5个不同的数据源(数据库、缓存、文件等)加载商品数据,主线程必须等待所有数据源加载完成后,才能汇总展示。
(1)代码实现
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 初始化计数器(需要等待5个线程完成)
CountDownLatch latch = new CountDownLatch(5);
// 启动5个数据加载线程
for (int i = 1; i <= 5; i++) {
new Thread(() -> {
try {
// 模拟数据加载(耗时1~3秒)
Thread.sleep((long) (Math.random() * 2000 + 1000));
System.out.println(Thread.currentThread().getName() + " 数据加载完成");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown(); // 加载完成,计数器减1
}
}, "数据源-" + i).start();
}
// 主线程等待所有数据加载完成(最多等待5秒)
boolean allCompleted = latch.await(5, TimeUnit.SECONDS);
if (allCompleted) {
System.out.println("所有数据加载完成,开始汇总...");
} else {
System.out.println("超时!部分数据未加载完成");
}
}
}
(2)输出结果(示例)
数据源-2 数据加载完成
数据源-1 数据加载完成
数据源-5 数据加载完成
数据源-3 数据加载完成
数据源-4 数据加载完成
所有数据加载完成,开始汇总...
(3)关键说明
- 计数器初始值为5,每个子线程完成后调用
countDown()
,计数器减到0时,await()
返回; await(long timeout, TimeUnit unit)
可避免无限等待(如某个子线程卡死,主线程5秒后超时);- 计数器不可重置(归零后无法再次使用),适合“一次性”等待场景。
二、CyclicBarrier:循环屏障,多线程齐步走
2.1 核心概念与原理
CyclicBarrier
(循环屏障)允许一组线程相互等待,直到所有线程都到达“屏障点”后,再继续执行后续操作。与CountDownLatch
不同,它的计数器可重复使用(通过reset()
方法重置),适合多阶段任务同步。
核心方法:
CyclicBarrier(int parties)
:构造函数,设置需要等待的线程数(parties
);CyclicBarrier(int parties, Runnable barrierAction)
:构造函数,增加一个屏障触发时的回调(所有线程到达后执行);int await()
:线程到达屏障点后等待(返回当前线程的到达顺序);void reset()
:重置屏障(计数器归零,未到达的线程抛出BrokenBarrierException
)。
2.2 实战示例:多阶段任务的循环同步
场景:一个分布式计算任务需要分3阶段执行(数据校验→计算→汇总),每个阶段必须等待所有计算节点完成当前阶段,才能进入下一阶段。
(1)代码实现
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("所有节点完成当前阶段,进入下一阶段...")
);
// 启动3个计算节点线程
for (int i = 1; i <= 3; i++) {
new Thread(() -> {
try {
// 阶段1:数据校验
System.out.println(Thread.currentThread().getName() + " 完成数据校验");
barrier.await(); // 等待其他节点完成阶段1
// 阶段2:执行计算
System.out.println(Thread.currentThread().getName() + " 完成计算");
barrier.await(); // 等待其他节点完成阶段2
// 阶段3:结果汇总
System.out.println(Thread.currentThread().getName() + " 完成汇总");
barrier.await(); // 等待其他节点完成阶段3
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}, "计算节点-" + i).start();
}
}
}
(2)输出结果(示例)
计算节点-1 完成数据校验
计算节点-2 完成数据校验
计算节点-3 完成数据校验
所有节点完成当前阶段,进入下一阶段...
计算节点-1 完成计算
计算节点-2 完成计算
计算节点-3 完成计算
所有节点完成当前阶段,进入下一阶段...
计算节点-1 完成汇总
计算节点-2 完成汇总
计算节点-3 完成汇总
所有节点完成当前阶段,进入下一阶段...
(3)关键说明
- 屏障初始等待线程数为3,每个线程执行到
await()
时会阻塞,直到3个线程都到达; - 每次所有线程到达后,触发
barrierAction
(阶段总结),然后计数器重置,支持下一阶段使用; - 若某个线程在
await()
时被中断或超时,会抛出BrokenBarrierException
,其他线程也会终止等待。
三、CountDownLatch vs CyclicBarrier:核心差异与选择指南
特性 | CountDownLatch | CyclicBarrier |
---|---|---|
计数器方向 | 递减(初始值→0) | 递增(0→parties) |
可重置性 | 不可重置(一次性使用) | 可重置(通过reset() 重复使用) |
等待线程角色 | 一个/多个线程等待其他线程完成 | 多个线程互相等待(齐步走) |
典型场景 | 主线程等待子任务完成(如数据汇总) | 多阶段任务同步(如分阶段计算) |
回调支持 | 无(需手动触发) | 有(所有线程到达后执行barrierAction ) |
选择建议:
- 若只需“一次性”等待子任务完成(如初始化资源),选
CountDownLatch
; - 若需多阶段任务同步(如游戏加载→战斗→结算),选
CyclicBarrier
; - 若需要“等待N个线程完成”且支持重复使用,优先
CyclicBarrier
。
四、避坑指南:常见错误与解决方案
4.1 CountDownLatch的“计数器溢出”
错误场景:初始化计数器为5,但实际调用了6次countDown()
,导致计数器变为-1(无异常,但await()
会立即返回)。
解决方案:确保countDown()
调用次数不超过初始值(可通过日志监控调用次数)。
4.2 CyclicBarrier的“屏障损坏”
错误场景:某个线程在await()
时被中断,导致屏障状态变为BROKEN
,其他线程调用await()
时抛出BrokenBarrierException
。
解决方案:
- 捕获
BrokenBarrierException
并处理(如重试或终止任务); - 使用
reset()
重置屏障(需确保所有线程已终止)。
4.3 混合使用导致的死锁
错误场景:在CyclicBarrier
的barrierAction
中再次调用await()
,导致线程无法释放。
解决方案:barrierAction
应是一个轻量级操作(如日志记录、状态更新),避免阻塞。
五、实战扩展:结合线程池的高效协作
实际开发中,CountDownLatch
和CyclicBarrier
常与线程池(如ExecutorService
)结合使用,提升资源利用率。
5.1 示例:线程池+CountDownLatch实现批量任务
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolWithLatch {
public static void main(String[] args) throws InterruptedException {
int taskCount = 10;
CountDownLatch latch = new CountDownLatch(taskCount);
ExecutorService pool = Executors.newFixedThreadPool(3); // 3个线程处理10个任务
for (int i = 0; i < taskCount; i++) {
pool.submit(() -> {
try {
Thread.sleep(500); // 模拟任务执行
System.out.println(Thread.currentThread().getName() + " 完成任务");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown();
}
});
}
latch.await(); // 等待所有任务完成
pool.shutdown();
System.out.println("所有任务完成,线程池关闭");
}
}
5.2 输出结果(示例)
pool-1-thread-1 完成任务
pool-1-thread-2 完成任务
pool-1-thread-3 完成任务
...(重复输出)
所有任务完成,线程池关闭
结语:并发协作的“黄金搭档”
CountDownLatch
和CyclicBarrier
是Java并发包中解决线程同步问题的核心工具。前者适合“一次性等待”场景(如主线程等待子任务),后者适合“多阶段齐步走”场景(如分阶段计算)。掌握它们的核心差异和使用技巧,能让你在多线程开发中高效解决协作问题,避免死锁和资源浪费。
下一次遇到多线程同步需求时,不妨问自己:“用CountDownLatch还是CyclicBarrier更合适?”——这可能是优化并发性能的关键一步!