CountDownLatch 和 CyclicBarrier 使用场景详解
在并发编程中,Java 提供了多种工具来帮助管理线程之间的协调。CountDownLatch
和 CyclicBarrier
是两种常用的同步工具类,它们虽然功能不同,但都用于线程之间的协调与同步。
1. CountDownLatch 使用场景
CountDownLatch
是一个线程同步工具,它允许一个或多个线程等待其他线程完成一组操作。这种机制非常适用于以下场景:
- 并行任务的等待:当你有多个子任务需要并行执行,并且在所有子任务完成后再继续执行主任务时,
CountDownLatch
是理想的选择。 - 一次性事件触发:某些场景下,你可能需要在特定数量的操作完成后触发某个事件,比如多线程的初始化工作。
代码示例:并行任务的等待
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
private static final int TASK_COUNT = 3;
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(TASK_COUNT);
for (int i = 0; i < TASK_COUNT; i++) {
new Thread(new Task(latch)).start();
}
// 等待所有任务完成
latch.await();
System.out.println("所有任务已完成,继续主线程工作");
}
static class Task implements Runnable {
private final CountDownLatch latch;
Task(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 正在执行任务...");
// 模拟任务耗时操作
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println(Thread.currentThread().getName() + " 任务完成");
latch.countDown(); // 每个线程完成后将计数器减1
}
}
}
源码分析
CountDownLatch
的核心在于它维护了一个计数器,该计数器在初始化时设置为指定的数量。每当一个线程完成任务后,会调用 countDown()
方法将计数器减1。当计数器归零时,所有在 await()
方法上等待的线程将继续执行。
public void countDown() {
sync.releaseShared(1);
}
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
countDown()
方法:通过sync.releaseShared(1)
来递减计数器。await()
方法:调用sync.acquireSharedInterruptibly(1)
来阻塞当前线程,直到计数器为零。
场景还原
场景一
假设你有一个任务需要从多个数据源中获取数据,并在所有数据都获取到之后进行处理。在这种场景下,CountDownLatch
可以确保主线程在所有数据获取完毕之后再开始处理。
场景二:大数据处理中的分布式计算同步
在大数据处理场景中,通常需要将一个大任务分解为多个小任务,并分布在多个节点上进行计算。每个计算节点在完成自己的部分任务后,都需要等待其他节点完成,然后进行数据的聚合。CyclicBarrier 可以用于这种场景,确保所有节点都完成任务后再继续下一步的聚合处理。
逻辑图:
2. CyclicBarrier 使用场景
CyclicBarrier
也是一个线程同步工具,它允许一组线程相互等待,直到所有线程都到达一个共同的屏障点。与 CountDownLatch
不同,CyclicBarrier
可以在屏障点被释放后再次使用,因此适用于多次使用的场景。
- 分段任务处理:当任务被分成多个阶段,每个阶段都需要所有线程完成后才能继续下一个阶段时,
CyclicBarrier
是最佳选择。 - 多线程模拟:在模拟多线程同时开始的场景中,比如多线程并发测试。
代码示例:分段任务处理
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
private static final int THREAD_COUNT = 3;
private static CyclicBarrier barrier = new CyclicBarrier(THREAD_COUNT, () -> {
System.out.println("所有任务准备完毕,开始执行下一阶段");
});
public static void main(String[] args) {
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(new Task()).start();
}
}
static class Task implements Runnable {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " 正在执行第一阶段...");
Thread.sleep(1000); // 模拟第一阶段任务
barrier.await(); // 等待其他线程完成第一阶段
System.out.println(Thread.currentThread().getName() + " 正在执行第二阶段...");
Thread.sleep(1000); // 模拟第二阶段任务
barrier.await(); // 等待其他线程完成第二阶段
} catch (InterruptedException | BrokenBarrierException e) {
Thread.currentThread().interrupt();
}
}
}
}
源码分析
CyclicBarrier
的内部维护了一个计数器,当所有参与线程都调用 await()
方法并达到屏障点时,计数器归零,并触发 Runnable
任务。
public int await() throws InterruptedException, BrokenBarrierException {
return dowait(false, 0L);
}
private int dowait(boolean timed, long nanos) throws InterruptedException, BrokenBarrierException, TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 计数器减1,直到为0
final Generation g = generation;
final int index = --count;
if (index == 0) { // 所有线程都已到达
nextGeneration(); // 进入下一代
return 0;
}
...
} finally {
lock.unlock();
}
}
await()
方法:通过dowait
方法实现线程的同步等待,并在计数器归零后重置屏障,进入下一代。nextGeneration()
方法:重置count
和generation
,使得屏障可以重新使用。
场景还原
场景一
假设你在开发一个复杂的算法,其分为多个步骤,每个步骤都需要多个线程协同工作并在同一时间点完成。CyclicBarrier
可以确保所有线程都在完成当前步骤后,统一进入下一步骤。
场景二:模拟多人游戏中的回合制同步
在多人在线游戏中,常常需要在回合制游戏中同步所有玩家的行动结果,然后进入下一个回合。例如,多个玩家在同一回合中选择动作,所有玩家的动作都完成后,统一执行这些动作,再进入下一回合。
逻辑图:
Player1
|
(CyclicBarrier.await)
Player2
|
(CyclicBarrier.await)
Player3
|
(CyclicBarrier.await)
|
+---------------------------+
| 执行本回合的所有玩家动作 |
+---------------------------+
|
+---------------------------+
| 准备下一个回合 |
+---------------------------+
|
(CyclicBarrier.await)
业务逻辑:
- 每个玩家选择动作并准备好后调用
CyclicBarrier.await()
。 - 当所有玩家都准备好后,游戏服务器执行所有玩家的动作。
- 进入下一回合,重复上述步骤。
场景三:多线程模拟交易系统的负载测试
在金融交易系统中,经常需要进行负载测试,以模拟高并发情况下系统的表现。假设要模拟多用户同时提交交易请求,可以使用 CyclicBarrier
来同步多个模拟用户,使它们在同一时刻开始交易操作,从而测试系统在高负载下的性能表现。
逻辑图:
User1
|
(CyclicBarrier.await)
User2
|
(CyclicBarrier.await)
User3
|
(CyclicBarrier.await)
|
+---------------------------+
| 同时提交交易请求 |
+---------------------------+
|
+---------------------------+
| 交易系统处理 |
+---------------------------+
业务逻辑:
- 多个线程分别模拟不同用户,准备好交易请求后调用
CyclicBarrier.await()
。 - 所有用户都准备好后,系统在同一时间接收到所有交易请求。
- 交易系统处理请求,并记录性能数据,用于分析系统在高负载下的表现。
3. 注意点
CountDownLatch
是一次性的,计数器一旦归零便不能重置;而CyclicBarrier
是可以重复使用的。- 在使用
CountDownLatch
时,确保所有countDown()
调用都能正确执行,否则会导致await()
永远阻塞。 - 在使用
CyclicBarrier
时,注意处理BrokenBarrierException
异常,该异常通常在屏障被破坏时抛出,如有线程中途退出。