CyclicBarrier
CyclicBarrier 源码分析
可循环的”栅栏”: N 个线程互相等待, 都到齐了才一起放行, 放行后自动复位可再用。它不走 AQS 共享模式, 而是用一把 ReentrantLock + 一个 Condition 把”计数、回调、唤醒”全部收进临界区, 再用一个 Generation 对象标识”第几轮”, 换代即放行。
代码块收起展开
// 基于 JDK 17, java.util.concurrent.CyclicBarrier
public class CyclicBarrier {
// 每一轮汇合用一个 Generation 对象标识。放行(trip)或 reset 都会换新一代;
// 等待线程靠 "generation 引用还是不是我进来时那个" 判断本轮是否已结束
private static class Generation {
Generation() {}
boolean broken; // 本代是否被打破(中断/超时/回调抛异常/reset), 只在持锁时读写, 不需要 volatile
}
private final ReentrantLock lock = new ReentrantLock(); // 一把独占锁保护全部可变状态
private final Condition trip = lock.newCondition(); // 未到齐的线程全挂在这一个条件队列上
private final int parties; // 参与方总数 N, 不可变 —— 可循环的前提是它和 count 分开存
private final Runnable barrierCommand; // 到齐后、放行前执行的回调, 可为 null
private Generation generation = new Generation();
private int count; // 本代还差几个线程; 从 parties 减到 0, 换代或打破时复位
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction; // 典型用法: 所有分片算完后在这里合并结果
}
public CyclicBarrier(int parties) {
this(parties, null);
}
// ...
}核心逻辑全部在 dowait 一个方法里, 两个 await 变体只是换参数:
代码块收起展开
// 基于 JDK 17, java.util.concurrent.CyclicBarrier
// 放行 + 复位: "可循环"的全部实现就这三行
private void nextGeneration() {
trip.signalAll(); // 唤醒本代所有等待者
count = parties; // 计数复位, 下一轮直接可用
generation = new Generation(); // 换新对象而非清标志位 —— 老一代用引用比较就能判断"已放行"
}
// 打破当前代: broken 置位 + 复位 + 唤醒所有人(醒来后各自抛 BrokenBarrierException)
private void breakBarrier() {
generation.broken = true;
count = parties;
trip.signalAll();
}
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock(); // 全程持锁, --count 和后续决策天然原子, 不需要任何 CAS
try {
final Generation g = generation; // 先把"我这一代"存进局部变量, 之后判断换代全靠它
if (g.broken)
throw new BrokenBarrierException(); // 别人已经打破本代, 直接失败
if (Thread.interrupted()) { // 带着中断标记进门: 自己打破本代, 连累所有等待者
breakBarrier();
throw new InterruptedException();
}
int index = --count;
if (index == 0) { // 我是最后一个到达者 -> 触发放行
Runnable command = barrierCommand;
if (command != null) {
try {
command.run(); // 回调在最后到达的线程上、放行前同步执行
} catch (Throwable ex) {
breakBarrier(); // 回调抛异常必须打破整代, 否则其余线程永远等不到 signalAll
throw ex;
}
}
nextGeneration();
return 0; // 最后到达者的名次固定是 0
}
// 不是最后一个: 循环等待, 出口只有四个 —— 放行 / 被打破 / 中断 / 超时
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 {
// 本代其实已结束(换代或已 broken), 这个中断被认定"属于后续代码":
// 补一个中断标记后继续走正常出口, 不在这里抛 InterruptedException
Thread.currentThread().interrupt();
}
}
if (g.broken)
throw new BrokenBarrierException();
if (g != generation) // 换代了 = 本代正常放行; index 是到达名次(N-1 最早, 1 倒数第二)
return index;
if (timed && nanos <= 0L) {
breakBarrier(); // 超时不是自己悄悄退出, 而是打破整代
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}对外 API 都是 dowait 的薄封装, 加上一个容易被忽略的 reset:
代码块收起展开
// 基于 JDK 17, java.util.concurrent.CyclicBarrier
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // 不带超时的等待不可能抛 TimeoutException, 走到这说明 JDK 自身有 bug
}
}
public int await(long timeout, TimeUnit unit)
throws InterruptedException,
BrokenBarrierException,
TimeoutException {
return dowait(true, unit.toNanos(timeout));
}
// reset = 打破旧代 + 立即开新代: 正在等待的线程全部抛 BrokenBarrierException
public void reset() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
breakBarrier();
nextGeneration();
} finally {
lock.unlock();
}
}
public int getNumberWaiting() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return parties - count; // 读也要加锁: count 不是 volatile, 可见性全靠这把锁
} finally {
lock.unlock();
}
}原理串讲
设 3 个线程用同一个 barrier。线程 A 调 await(), 进 dowait, 先 lock.lock(), 把当前 generation 存进局部变量 g, 通过 broken 与中断两道门检查后 --count 得 index = 2, 不是 0, 于是进入死循环调 trip.await() —— 这一步会释放锁并挂起, 所以线程 B 才能进来重复同样的流程 (index = 1)。
线程 C 最后到达, --count 减到 0: 它先在自己身上同步执行 barrierCommand, 再调 nextGeneration() 做三件事 —— trip.signalAll() 唤醒 A 和 B、count = parties 复位、generation = new Generation() 换代 —— 然后返回 0。
A 和 B 被唤醒后依次重新拿到锁, 从 trip.await() 返回, 发现 g != generation, 各自带着自己的 index 返回。
此时 barrier 的状态和刚 new 出来时完全一样, 下一轮直接可用。
为什么用 ReentrantLock + Condition 而不是像 CountDownLatch 那样用 AQS 共享模式? 因为语义要求”最后一个到达者先执行回调、再统一放行”, 且任何一方出事要能打破整代 —— 这些都需要在”计数减到 0”和”唤醒”之间插入任意逻辑。
AQS 共享模式里 releaseShared 是一锤子买卖, 插不进回调和 broken 判定; 而在独占锁的临界区内, --count、跑回调、signalAll 天然是一个原子序列, 一行 CAS 都不用写。
锁与条件队列的机制见 ReentrantLock。
为什么每轮换一个新 Generation 对象, 而不是复用一个标志位或轮次计数? 等待线程从 trip.await() 醒来时, 世界可能已经变了好几轮 (锁的分配顺序不确定, 一个线程可能睡过多次放行)。
它只需要拿自己进门时抓的引用 g 和当前 generation 做一次 != 比较, 就能可靠判断”我那轮结束没有”, 不存在计数回绕或标志位被下一轮覆盖的问题; broken 也随对象走, 老一代被打破不会污染新一代。
这正是 nextGeneration 里 new Generation() 一行的全部意义, 也是它和一次性的 CountDownLatch 在结构上的根本差异: latch 的 state 减到 0 就死了, barrier 的 count 每代复位、代际用对象隔开。
为什么一个线程中断/超时要连累所有人? 栅栏的契约是”N 个人到齐才放行”。少了一个, 这次汇合永远凑不齐, 让其余线程继续等就是死等。
所以真源选择 all-or-none: 出事的线程调 breakBarrier() 置位 broken 并 signalAll, 其他线程醒来看到 g.broken 各自抛 BrokenBarrierException, 快速失败好过静默挂死。
设计取舍
- all-or-none 打破模型: 不支持”少一个人也放行”的弹性到齐数, 需要动态增减参与方时该用 Phaser。
await的返回值是到达名次 (parties - 1最早, 0 最后), 可以用if (barrier.await() == 0)选一个线程干收尾活, 替代barrierCommand。- 回调在最后到达的线程上同步执行: 回调有多慢, 所有人就多等多久; 回调抛异常整代作废。
reset只适合没人出事时的主动复位; broken 之后再 reset 需要线程间额外协调选人来调, Javadoc 直说不如新建一个 barrier。count与parties分开存是”可循环”的前提; 状态可见性全靠锁, 类里没有一个 volatile 字段。