CyclicBarrier

CyclicBarrier 源码分析

可循环的”栅栏”: N 个线程互相等待, 都到齐了才一起放行, 放行后自动复位可再用。它不走 AQS 共享模式, 而是用一把 ReentrantLock + 一个 Condition 把”计数、回调、唤醒”全部收进临界区, 再用一个 Generation 对象标识”第几轮”, 换代即放行。

代码块JAVA · 31 行收起展开
// 基于 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 变体只是换参数:

代码块JAVA · 79 行收起展开
// 基于 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:

代码块JAVA · 37 行收起展开
// 基于 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 与中断两道门检查后 --countindex = 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 也随对象走, 老一代被打破不会污染新一代。
这正是 nextGenerationnew 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。
  • countparties 分开存是”可循环”的前提; 状态可见性全靠锁, 类里没有一个 volatile 字段。

延伸阅读