CyclicBarrier 是 Java 并发包(java.util.concurrent)中提供的一个同步辅助类,它允许一组线程相互等待,直到所有线程都到达某个公共屏障点(barrier point)后才能继续执行。与 CountDownLatch 不同,CyclicBarrier 具有可重用的特性(即“Cyclic”),当所有线程通过屏障后,计数器会自动重置,可以用于下一轮同步。此外,它还支持在屏障触发时执行一个可选的 Runnable 屏障动作。
CyclicBarrier 适用于多线程分阶段计算、模拟并发任务同时开始、以及需要反复进行多轮同步的场景。本文基于 JDK 8 源码,从核心方法、核心特性与实现原理、流程时序图、实际应用场景、吞吐量分析以及注意事项六个方面对 CyclicBarrier 进行深入解析,帮助读者正确理解并使用这一并发工具。
0. 核心方法说明
| 方法签名 | 描述 | 参数/返回值 | 异常 |
|---|---|---|---|
CyclicBarrier(int parties) | 构造一个屏障,需要等待 parties 个线程到达。 | parties:需要等待的线程数量(必须 > 0) | IllegalArgumentException(如果 parties <= 0) |
CyclicBarrier(int parties, Runnable barrierAction) | 构造一个屏障,当所有线程到达后,由最后一个到达的线程执行 barrierAction。 | barrierAction:屏障触发时执行的动作(可为 null) | IllegalArgumentException(如果 parties <= 0) |
int await() | 当前线程到达屏障点,等待其他线程。若当前是最后一个到达的线程,则触发屏障并唤醒所有等待线程。 | 返回当前线程到达的序号:0 表示最后一个线程,parties-1 表示第一个线程 | InterruptedException:等待时被中断;BrokenBarrierException:屏障已破损 |
int await(long timeout, TimeUnit unit) | 带超时的 await(),如果指定时间内仍未等到所有线程到达,当前线程会抛出 TimeoutException,并且屏障会进入破损状态。 | timeout:超时时间;unit:时间单位;返回值同 await() | InterruptedException,BrokenBarrierException,TimeoutException(超时后抛出) |
void reset() | 重置屏障到初始状态。如果已有线程正在等待,它们会抛出 BrokenBarrierException。 | 无 | 无 |
int getNumberWaiting() | 获取当前正在屏障处等待的线程数。 | 等待线程数量 | 无 |
boolean isBroken() | 判断当前屏障是否处于破损状态。 | true:已破损;false:正常 | 无 |
说明:
await()方法返回的int值可以用于“选举”最后一个线程执行特殊操作,不过更推荐使用barrierAction。- 屏障一旦破损(由于中断、超时、重置或屏障动作异常),所有后续对
await()的调用都会立即抛出BrokenBarrierException,直到屏障被重置。
1. 核心特性及其实现原理(结合 JDK 8 源码)
特性一:计数等待(所有线程到达屏障前阻塞)
描述:每个线程调用 await() 会使内部计数器减 1,若计数器未到 0,线程阻塞;直到最后一个线程将计数器减为 0,所有等待线程被唤醒。
JDK 8 源码实现(关键字段与 dowait 方法):
public class CyclicBarrier {
/** 用于互斥的锁 */
private final ReentrantLock lock = new ReentrantLock();
/** 条件队列,用于等待线程 */
private final Condition trip = lock.newCondition();
/** 需要等待的线程总数(构造时设定,不变) */
private final int parties;
/** 当前剩余未到达的线程数(初始为 parties) */
private int count;
/** 屏障的代次,用于支持重用和破损状态 */
private Generation generation = new Generation();
private static class Generation {
boolean broken = false; // 当前代次是否已破损
}
/** 屏障动作(可选) */
private final Runnable barrierCommand;
// 构造器
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // 不可能发生
}
}
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException, TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock(); // 1. 加锁
try {
final Generation g = generation;
if (g.broken)
throw new BrokenBarrierException();
if (Thread.interrupted()) {
breakBarrier(); // 中断导致屏障破损
throw new InterruptedException();
}
int index = --count; // 2. 计数器减1
if (index == 0) { // 3. 最后一个线程到达
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
command.run(); // 执行屏障动作
ranAction = true;
nextGeneration(); // 4. 唤醒所有等待线程,重置计数,进入下一代
return 0;
} finally {
if (!ranAction)
breakBarrier(); // 动作执行失败则破损屏障
}
}
// 非最后一个线程:自旋等待(实际上通过条件队列阻塞)
for (;;) {
try {
if (!timed)
trip.await(); // 进入条件队列,释放锁
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
// 如果当前代次未破损且自己属于当前代,则破损并抛出异常
if (g == generation && !g.broken) {
breakBarrier();
throw ie;
} else {
// 属于下一代,则只是中断当前线程
Thread.currentThread().interrupt();
}
}
if (g.broken)
throw new BrokenBarrierException();
if (g != generation)
return index; // 已进入下一代,返回索引
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
private void breakBarrier() {
generation.broken = true;
count = parties; // 重置计数(虽然破损,但便于后续 reset)
trip.signalAll();
}
private void nextGeneration() {
trip.signalAll(); // 唤醒所有等待线程
count = parties; // 重置计数器
generation = new Generation(); // 新建代次
}
}
- 核心逻辑:锁保护
count和generation。最后一个线程执行nextGeneration(),唤醒所有等待线程并重置状态。其他线程在trip.await()上阻塞。
特性二:可重用(Cyclic)
描述:屏障被触发(所有线程到达)或手动调用 reset() 后,可以重新使用,计数器恢复为初始值,等待线程进入新的一代。
源码实现:通过 nextGeneration() 创建新的 Generation 对象,并重置 count = parties。reset() 方法先破损当前代,再调用 nextGeneration():
public void reset() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
breakBarrier(); // 破损当前代,让等待线程抛出异常
nextGeneration(); // 开启下一代(count已重置)
} finally {
lock.unlock();
}
}
- 重用原理:每一轮屏障使用独立的
Generation对象,旧代次中的线程被唤醒后检查g != generation而退出,新来的线程使用新Generation。
特性三:可设置屏障动作(Barrier Action)
描述:通过构造函数传入 Runnable,当最后一个线程到达时,在唤醒其他线程之前执行该动作。
源码实现:见 dowait 中 index == 0 分支,执行 barrierCommand.run()。该动作在最后一个线程的上下文中同步执行,执行时仍持有锁。
特性四:支持线程中断和超时
描述:await() 可响应中断;await(long timeout, TimeUnit unit) 支持超时,超时后屏障进入破损状态。
源码实现:dowait 方法接收 timed 和 nanos 参数。等待时使用 trip.await() 或 trip.awaitNanos(nanos)。超时发生后调用 breakBarrier(),将 generation.broken 设为 true,其他线程会抛出 BrokenBarrierException。
特性五:状态查询
描述:getNumberWaiting() 返回当前等待的线程数;isBroken() 判断屏障是否破损。
JDK 8 源码:
public int getNumberWaiting() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return parties - count;
} finally {
lock.unlock();
}
}
public boolean isBroken() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return generation.broken;
} finally {
lock.unlock();
}
}
2. 必要流程
(图与之前相同,因为 JDK 8 的行为完全相同,无需修改)
sequenceDiagram
participant T1 as Thread-1
participant T2 as Thread-2
participant T3 as Thread-3
participant CB as CyclicBarrier
participant Lock as ReentrantLock
participant Cond as Condition(trip)
T1->>CB: await()
CB->>Lock: lock()
CB->>CB: count = 2 (原3->2)
CB->>Cond: await() (释放锁并阻塞)
CB->>Lock: unlock() (await内部释放)
T2->>CB: await()
CB->>Lock: lock()
CB->>CB: count = 1
CB->>Cond: await()
CB->>Lock: unlock()
T3->>CB: await()
CB->>Lock: lock()
CB->>CB: count = 0 (最后一个)
alt 存在屏障动作
CB->>CB: barrierCommand.run()
end
CB->>Cond: signalAll()
CB->>CB: nextGeneration() (count重置为3, 新Generation)
CB->>Lock: unlock()
T3-->>T3: 返回(0)
Cond-->>T1: 被唤醒
T1->>Lock: lock() (重新获取锁)
T1->>CB: 检查 generation 已变,返回 index
T1->>Lock: unlock()
Cond-->>T2: 被唤醒
T2->>Lock: lock()
T2->>CB: 检查 generation 已变,返回 index
T2->>Lock: unlock()
详细描述(基于 JDK 8 源码行为):
- 初始
count = parties = 3,generation.broken = false。 - T1 获得锁,
count变为 2,非最后一个,执行trip.await()释放锁并阻塞。 - T2 类似,
count变为 1,阻塞。 - T3 获得锁,
count变为 0,触发屏障:- 若有
barrierCommand,在 T3 线程中运行。 - 调用
nextGeneration():先trip.signalAll()唤醒 T1、T2,然后重置count=3,新建Generation。 - T3 返回 0。
- 若有
- T1、T2 被唤醒后重新竞争锁,获取锁后发现自己属于旧
Generation,而当前generation已改变,因此返回各自的index(1 和 2)。 - 所有线程通过屏障,屏障实例可被下一轮使用。
3. 实际应用场景与代码举例(JDK 8 兼容)
场景一:多线程分阶段计算(MapReduce 风格)
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierSumExample {
private static final int THREAD_COUNT = 4;
private static final int ARRAY_SIZE = 100;
private static int[] data = new int[ARRAY_SIZE];
private static int[] partialSums = new int[THREAD_COUNT];
private static int finalSum = 0;
static {
for (int i = 0; i < ARRAY_SIZE; i++) {
data[i] = i + 1;
}
}
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(THREAD_COUNT, () -> {
int total = 0;
for (int ps : partialSums) {
total += ps;
}
finalSum = total;
System.out.println("Barrier action: sum = " + finalSum);
});
for (int i = 0; i < THREAD_COUNT; i++) {
final int threadId = i;
new Thread(() -> {
int start = threadId * (ARRAY_SIZE / THREAD_COUNT);
int end = (threadId == THREAD_COUNT - 1) ? ARRAY_SIZE : (threadId + 1) * (ARRAY_SIZE / THREAD_COUNT);
int sum = 0;
for (int j = start; j < end; j++) {
sum += data[j];
}
partialSums[threadId] = sum;
System.out.println(Thread.currentThread().getName() + " computed partial sum: " + sum);
try {
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}, "Worker-" + i).start();
}
// 简单等待所有工作线程结束(生产环境应使用 join 或 CountDownLatch)
try { Thread.sleep(1000); } catch (InterruptedException e) {}
System.out.println("Final sum from main: " + finalSum);
}
}
场景二:模拟并发任务同时开始(可重用)
CyclicBarrier startLine = new CyclicBarrier(5, () -> System.out.println("All ready, go!"));
for (int i = 0; i < 5; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " ready");
try {
startLine.await();
System.out.println(Thread.currentThread().getName() + " running");
} catch (Exception e) {}
}, "Runner-" + i).start();
}
4. 吞吐量分析(与 JDK 版本无关)
影响 CyclicBarrier 吞吐量的核心因素不变:
-
锁竞争开销
每个await()都要获取ReentrantLock。高并发下锁争用导致串行化,降低吞吐量。
Why:ReentrantLock的lock/unlock涉及 CAS 和队列维护,大量线程同时竞争时,只有少数线程能快速通过,其余被阻塞在同步队列中。 -
条件等待与唤醒开销
最后一个线程调用signalAll会将所有等待线程从条件队列转移到锁的同步队列,之后每个线程需重新竞争锁。大量线程同时唤醒会造成“唤醒风暴”,增加 CPU 负担和上下文切换。
Why:每个被唤醒的线程都会经历从阻塞到可运行的状态转换,并且竞争锁的过程本身也需要时间。 -
屏障动作执行时间
若barrierAction耗时较长,会延长锁的持有时间,使得被唤醒的线程在锁队列中等待更久,从而降低系统整体吞吐量。
Why:锁一直被最后一个线程持有,其他线程即使已被signalAll也无法立即获得锁。 -
线程上下文切换
线程在trip.await()阻塞时会被调度出 CPU,唤醒时重新调度。频繁阻塞/唤醒增加上下文切换开销。
Why:上下文切换需要保存和恢复线程状态,开销可达数微秒至数十微秒,对于细粒度任务影响显著。 -
重用性的正面影响
相比每次新建CountDownLatch,CyclicBarrier可重用避免了重复创建对象和初始化计数器的开销,在多轮同步场景中能提升吞吐量。
吞吐量公式(理想化模型)
设每个线程任务执行时间为 ( T_{work} ),同步开销(含锁、条件等待、唤醒、屏障动作)为 ( T_{sync} ),线程数为 ( N ),轮数为 ( R )。总时间 ≈ ( R \times (T_{work} + T_{sync}) )。
吞吐量 = 完成的任务单元数 / 总时间 = ( (R \times N) / (R \times (T_{work} + T_{sync})) = N / (T_{work} + T_{sync}) )。
- 当 ( T_{work} \gg T_{sync} ) 时,吞吐量接近 ( N / T_{work} ),同步开销可忽略。
- 当 ( T_{work} ) 很小(微秒级)时,吞吐量由 ( T_{sync} ) 主导,使用
CyclicBarrier反而会严重降低性能。
结论:CyclicBarrier 适用于计算密集型、负载均衡的多阶段任务;不适合任务粒度极细或线程数量极大(如数千)的场景。
5. 注意事项及具体原因(基于 JDK 8 行为)
1. 线程中断会导致屏障破损(BrokenBarrierException)
注意:任何等待线程被中断,屏障都会进入破损状态,其他线程会抛出 BrokenBarrierException。
原因:dowait 中捕获 InterruptedException 后调用 breakBarrier(),设置 generation.broken = true 并唤醒所有线程。这是为了保证“全有或全无”的语义:一个线程失败,整体同步失败。
2. 重置屏障(reset)需谨慎处理正在等待的线程
注意:调用 reset() 时,如果已有线程在 await() 中,它们会立即收到 BrokenBarrierException。
原因:reset() 内部先调用 breakBarrier() 破损当前代,使旧代中的等待线程感知破损状态,避免它们永久等待。然后再调用 nextGeneration() 创建新代。
3. 屏障动作中的异常会导致屏障破损
注意:若屏障动作抛出未捕获异常,最后一个线程会抛出该异常(被包装在 BrokenBarrierException 中),其他线程收到 BrokenBarrierException。
原因:dowait 中执行 command.run() 时若发生异常,ranAction 保持 false,进入 finally 调用 breakBarrier()。屏障无法正常完成,必须破损。
4. 线程数量必须与 parties 匹配
注意:实际参与 await() 的线程数必须等于 parties,否则要么死锁(少于),要么多余的线程永远阻塞(多于)。
原因:计数器从 parties 递减到 0 才触发。若线程数不足,count 永远无法归零;若线程数过多,多出的线程进入等待后,计数器早已为 0 并进入下一代,它们会一直等待新代中的线程,而新代线程可能永远不会来。
5. 避免在屏障动作中再次调用 await()
注意:屏障动作中再次调用同一个 CyclicBarrier 的 await() 会导致死锁。
原因:屏障动作由最后一个线程执行,此时该线程尚未从 await() 返回,且屏障刚执行 nextGeneration() 进入新代。若动作中再次 await(),该线程会尝试在新代中等待,但其他线程尚未被唤醒(还在旧代的锁队列中),因此新代计数器无法归零,导致线程永久阻塞。
6. 内存可见性
注意:CyclicBarrier 本身不保证线程间共享变量的可见性,除非使用 volatile 或屏障动作。
原因:虽然 await() 内部使用了锁,但多个线程通过屏障后,谁先获得锁是不确定的。如果需要可靠的跨线程数据传递,应使用 volatile、AtomicReference 或将合并逻辑放在屏障动作中执行(屏障动作的 happens-before 规则保证了可见性)。
7. 超时后屏障破损的传播
注意:某个线程超时返回后,屏障进入破损状态,其他所有线程(包括未超时的)都会抛出 BrokenBarrierException。
原因:与中断类似,超时意味着该线程无法继续等待,破坏了屏障完整性,因此必须整体失效。
8. 性能考虑:避免大量创建 CyclicBarrier 实例
注意:若需要多轮同步,尽量重用同一个 CyclicBarrier,而不是每轮都 new 一个新实例。
原因:每个 CyclicBarrier 内部包含 ReentrantLock 和 Condition,关联了同步队列和条件队列。频繁创建销毁会增加 GC 压力和内存开销。重用并通过 reset()(需小心使用)或自然触发下一代可提升性能。
以上内容基于 JDK 8 源码(openjdk-8u40)进行分析和验证,涵盖了核心方法、特性原理、时序图、场景示例、吞吐量分析和注意事项,帮助读者全面掌握 CyclicBarrier 的使用与原理。