CyclicBarrier 是 Java 并发编程中一个强大的同步辅助类,它解决的问题场景非常明确:当我们需要让一组线程在某个执行点相互等待,直到所有线程都到达这个点后才能继续执行时,CyclicBarrier 就是最佳选择。
CyclicBarrier 的核心思想来源于现实生活中的集合点概念。想象一个旅行团的场景:导游规定所有游客必须在上午10点到达景区门口集合,只有所有人都到齐后才会开始游览。CyclicBarrier 就是程序世界中的这个"集合点"。
从技术实现角度看,CyclicBarrier 内部维护了:
与一次性使用的 CountDownLatch 不同,CyclicBarrier 的"循环"特性体现在:
这个特性使得 CyclicBarrier 特别适合需要重复进行多轮同步的场景,比如:
注意:虽然 CyclicBarrier 可以重用,但如果某个线程在 await() 时被中断或超时,会导致屏障进入"broken"状态,此时需要调用 reset() 方法才能继续使用。
让我们通过一个更贴近实际开发的例子来理解 CyclicBarrier 的使用。假设我们正在开发一个分布式数据处理的系统,需要等待所有工作节点完成数据加载后才能开始计算:
java复制public class DataProcessingTask implements Runnable {
private final CyclicBarrier barrier;
private final String workerName;
public DataProcessingTask(CyclicBarrier barrier, String workerName) {
this.barrier = barrier;
this.workerName = workerName;
}
@Override
public void run() {
try {
System.out.println(workerName + " 开始加载数据...");
// 模拟数据加载时间
Thread.sleep(1000 + (long)(Math.random() * 2000));
System.out.println(workerName + " 数据加载完成,等待其他节点...");
int arrivalIndex = barrier.await();
if (arrivalIndex == 0) {
System.out.println("所有节点数据准备就绪,开始计算...");
}
// 执行计算任务
doCompute();
} catch (Exception e) {
e.printStackTrace();
}
}
private void doCompute() {
// 实际计算逻辑
}
}
CyclicBarrier 的构造函数允许我们传入一个 Runnable 作为屏障动作,这个动作会在所有线程到达屏障后,由最后一个到达的线程执行。这个特性可以用来:
java复制// 使用屏障动作收集处理结果
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("所有线程处理完成,汇总结果...");
// 这里可以访问共享变量来汇总结果
});
// 工作线程可以这样使用
public void run() {
// 处理数据...
int result = processData();
// 将结果存入共享变量
sharedResults[threadIndex] = result;
barrier.await();
// 继续后续处理...
}
在实际应用中,我们必须考虑线程可能无法及时到达屏障的情况。CyclicBarrier 提供了带超时的 await 方法:
java复制try {
// 等待最多2秒
int index = barrier.await(2, TimeUnit.SECONDS);
} catch (TimeoutException e) {
// 处理超时
System.out.println("等待超时,屏障将被破坏");
barrier.reset(); // 重置屏障以便后续使用
} catch (BrokenBarrierException e) {
// 处理屏障被破坏的情况
System.out.println("屏障已被破坏,无法继续");
}
重要提示:当任何一个线程在 await 时抛出 TimeoutException 或 InterruptedException,屏障会进入 broken 状态,所有其他等待的线程会立即抛出 BrokenBarrierException。此时必须调用 reset() 方法才能继续使用该屏障。
CyclicBarrier 的实现主要依赖于以下几个关键组件:
java复制// 简化版内部结构示意
public class CyclicBarrier {
private final ReentrantLock lock = new ReentrantLock();
private final Condition trip = lock.newCondition();
private final int parties;
private final Runnable barrierCommand;
private Generation generation = new Generation();
private int count;
// 其他方法...
}
当线程调用 await() 时,实际执行的是内部的 dowait() 方法:
java复制private int dowait(boolean timed, long nanos) throws ... {
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 检查屏障状态...
// 减少计数器
int index = --count;
if (index == 0) { // 最后一个到达的线程
Runnable command = barrierCommand;
if (command != null) {
try {
command.run();
} catch (Throwable ex) {
breakBarrier();
throw ex;
}
}
nextGeneration();
return 0;
}
// 不是最后一个线程,进入等待
for (;;) {
try {
if (!timed)
trip.await();
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
// 处理中断...
}
// 检查屏障状态...
}
} finally {
lock.unlock();
}
}
reset() 方法的实现非常关键,它需要:
java复制public void reset() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
breakBarrier(); // 破坏当前屏障
nextGeneration(); // 开始新的屏障
} finally {
lock.unlock();
}
}
CyclicBarrier 特别适合处理需要多阶段同步的任务。例如,一个数据处理流水线可能包含:
每个阶段完成后都需要所有工作线程同步:
java复制public class PipelineTask implements Runnable {
private final CyclicBarrier[] barriers;
public void run() {
try {
// 阶段1:数据加载
loadData();
barriers[0].await();
// 阶段2:数据清洗
cleanData();
barriers[1].await();
// 阶段3:数据分析
analyzeData();
barriers[2].await();
// 阶段4:结果汇总
collectResults();
} catch (Exception e) {
handleException(e);
}
}
}
虽然 CyclicBarrier 非常有用,但在高并发场景下需要注意:
锁竞争:内部使用 ReentrantLock,当大量线程同时调用 await() 时会产生锁竞争
屏障动作耗时:屏障动作由最后一个到达的线程执行,如果动作耗时较长会延迟其他线程
线程数选择:屏障的 parties 数应该与可用CPU核心数匹配
当结合线程池使用时,需要特别注意:
java复制ExecutorService executor = Executors.newFixedThreadPool(4);
CyclicBarrier barrier = new CyclicBarrier(4); // 必须与线程池大小匹配
for (int i = 0; i < 4; i++) {
executor.submit(() -> {
try {
// 工作代码...
barrier.await();
} catch (Exception e) {
// 处理异常
}
});
}
警告:如果线程池大小小于屏障的 parties 数,会导致所有线程永久阻塞!因为永远不会有足够线程到达屏障。
当遇到 BrokenBarrierException 时,通常有以下原因:
排查步骤:
使用 CyclicBarrier 时可能出现的死锁场景:
预防措施:
监控 CyclicBarrier 使用情况的技巧:
java复制// 监控示例
if (barrier.getNumberWaiting() > threshold) {
logger.warn("屏障等待线程数过多: " + barrier.getNumberWaiting());
}
if (barrier.isBroken()) {
logger.error("屏障已破坏,需要处理");
}
虽然两者都用于线程同步,但设计目的不同:
CyclicBarrier:
CountDownLatch:
| 场景 | CyclicBarrier | CountDownLatch |
|---|---|---|
| 多线程初始化 | ✓ | ✓ |
| 分阶段处理 | ✓ | ✗ |
| 结果聚合 | ✓ | ✓ |
| 重复同步 | ✓ | ✗ |
| 主从模式 | ✗ | ✓ |
| 特性 | CyclicBarrier | CountDownLatch |
|---|---|---|
| 锁开销 | 较高(使用ReentrantLock) | 较低(使用AQS共享模式) |
| 可重用性 | ✓ | ✗ |
| 灵活性 | 较高(支持屏障动作) | 较低 |
| 异常处理 | 复杂(Broken状态) | 简单 |
在实际项目中,我通常会这样选择:
让我们看一个更复杂的实际应用案例:使用 CyclicBarrier 实现并行矩阵乘法。假设我们需要计算两个大矩阵的乘积,可以将计算任务分块并行处理:
java复制public class MatrixMultiplier {
private final CyclicBarrier barrier;
private final double[][] a, b, result;
private final int numThreads;
public MatrixMultiplier(double[][] a, double[][] b, int numThreads) {
this.a = a;
this.b = b;
this.numThreads = numThreads;
this.result = new double[a.length][b[0].length];
this.barrier = new CyclicBarrier(numThreads, this::mergeResults);
}
public double[][] multiply() {
ExecutorService executor = Executors.newFixedThreadPool(numThreads);
int rowsPerThread = a.length / numThreads;
for (int i = 0; i < numThreads; i++) {
int startRow = i * rowsPerThread;
int endRow = (i == numThreads - 1) ? a.length : startRow + rowsPerThread;
executor.execute(new Worker(startRow, endRow));
}
executor.shutdown();
try {
executor.awaitTermination(1, TimeUnit.HOURS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return result;
}
private class Worker implements Runnable {
private final int startRow, endRow;
private final double[][] partialResult;
Worker(int startRow, int endRow) {
this.startRow = startRow;
this.endRow = endRow;
this.partialResult = new double[endRow - startRow][b[0].length];
}
@Override
public void run() {
try {
// 计算分配的行范围
for (int i = startRow; i < endRow; i++) {
for (int j = 0; j < b[0].length; j++) {
for (int k = 0; k < b.length; k++) {
partialResult[i - startRow][j] += a[i][k] * b[k][j];
}
}
}
// 等待所有工作线程完成计算
barrier.await();
} catch (Exception e) {
Thread.currentThread().interrupt();
}
}
}
private void mergeResults() {
// 合并所有工作线程的部分结果
// 在实际实现中,这里可能需要从各Worker中收集partialResult
}
}
这个例子展示了 CyclicBarrier 在复杂计算任务中的应用:
经过多年使用 CyclicBarrier 的经验,我总结了以下最佳实践:
合理设置 parties 数:
始终处理中断和超时:
java复制try {
barrier.await();
} catch (InterruptedException e) {
// 恢复中断状态
Thread.currentThread().interrupt();
// 清理资源...
} catch (BrokenBarrierException e) {
// 处理屏障破坏情况
logger.error("屏障已破坏", e);
}
避免在持有锁时调用 await():
屏障动作保持轻量:
监控屏障状态:
考虑使用 Phaser 替代:
在实际项目中,我发现 CyclicBarrier 最常见的应用场景包括:
记住,CyclicBarrier 是一个强大的工具,但需要谨慎使用。正确的使用可以极大简化并发编程,而错误的使用则可能导致难以调试的问题。