CyclicBarrier详解

222 阅读12分钟

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)构造一个屏障,当所有线程到达后,由最后一个到达的线程执行 barrierActionbarrierAction:屏障触发时执行的动作(可为 nullIllegalArgumentException(如果 parties <= 0
int await()当前线程到达屏障点,等待其他线程。若当前是最后一个到达的线程,则触发屏障并唤醒所有等待线程。返回当前线程到达的序号:0 表示最后一个线程,parties-1 表示第一个线程InterruptedException:等待时被中断;BrokenBarrierException:屏障已破损
int await(long timeout, TimeUnit unit)带超时的 await(),如果指定时间内仍未等到所有线程到达,当前线程会抛出 TimeoutException,并且屏障会进入破损状态。timeout:超时时间;unit:时间单位;返回值同 await()InterruptedExceptionBrokenBarrierExceptionTimeoutException(超时后抛出)
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();    // 新建代次
    }
}
  • 核心逻辑:锁保护 countgeneration。最后一个线程执行 nextGeneration(),唤醒所有等待线程并重置状态。其他线程在 trip.await() 上阻塞。

特性二:可重用(Cyclic)

描述:屏障被触发(所有线程到达)或手动调用 reset() 后,可以重新使用,计数器恢复为初始值,等待线程进入新的一代。

源码实现:通过 nextGeneration() 创建新的 Generation 对象,并重置 count = partiesreset() 方法先破损当前代,再调用 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,当最后一个线程到达时,在唤醒其他线程之前执行该动作。

源码实现:见 dowaitindex == 0 分支,执行 barrierCommand.run()。该动作在最后一个线程的上下文中同步执行,执行时仍持有锁。

特性四:支持线程中断和超时

描述await() 可响应中断;await(long timeout, TimeUnit unit) 支持超时,超时后屏障进入破损状态。

源码实现dowait 方法接收 timednanos 参数。等待时使用 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 源码行为):

  1. 初始 count = parties = 3generation.broken = false
  2. T1 获得锁,count 变为 2,非最后一个,执行 trip.await() 释放锁并阻塞。
  3. T2 类似,count 变为 1,阻塞。
  4. T3 获得锁,count 变为 0,触发屏障:
    • 若有 barrierCommand,在 T3 线程中运行。
    • 调用 nextGeneration():先 trip.signalAll() 唤醒 T1、T2,然后重置 count=3,新建 Generation
    • T3 返回 0。
  5. T1、T2 被唤醒后重新竞争锁,获取锁后发现自己属于旧 Generation,而当前 generation 已改变,因此返回各自的 index(1 和 2)。
  6. 所有线程通过屏障,屏障实例可被下一轮使用。

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 吞吐量的核心因素不变:

  1. 锁竞争开销
    每个 await() 都要获取 ReentrantLock。高并发下锁争用导致串行化,降低吞吐量。
    WhyReentrantLocklock/unlock 涉及 CAS 和队列维护,大量线程同时竞争时,只有少数线程能快速通过,其余被阻塞在同步队列中。

  2. 条件等待与唤醒开销
    最后一个线程调用 signalAll 会将所有等待线程从条件队列转移到锁的同步队列,之后每个线程需重新竞争锁。大量线程同时唤醒会造成“唤醒风暴”,增加 CPU 负担和上下文切换。
    Why:每个被唤醒的线程都会经历从阻塞到可运行的状态转换,并且竞争锁的过程本身也需要时间。

  3. 屏障动作执行时间
    barrierAction 耗时较长,会延长锁的持有时间,使得被唤醒的线程在锁队列中等待更久,从而降低系统整体吞吐量。
    Why:锁一直被最后一个线程持有,其他线程即使已被 signalAll 也无法立即获得锁。

  4. 线程上下文切换
    线程在 trip.await() 阻塞时会被调度出 CPU,唤醒时重新调度。频繁阻塞/唤醒增加上下文切换开销。
    Why:上下文切换需要保存和恢复线程状态,开销可达数微秒至数十微秒,对于细粒度任务影响显著。

  5. 重用性的正面影响
    相比每次新建 CountDownLatchCyclicBarrier 可重用避免了重复创建对象和初始化计数器的开销,在多轮同步场景中能提升吞吐量。

吞吐量公式(理想化模型)

设每个线程任务执行时间为 ( 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()

注意:屏障动作中再次调用同一个 CyclicBarrierawait() 会导致死锁。

原因:屏障动作由最后一个线程执行,此时该线程尚未从 await() 返回,且屏障刚执行 nextGeneration() 进入新代。若动作中再次 await(),该线程会尝试在新代中等待,但其他线程尚未被唤醒(还在旧代的锁队列中),因此新代计数器无法归零,导致线程永久阻塞。

6. 内存可见性

注意CyclicBarrier 本身不保证线程间共享变量的可见性,除非使用 volatile 或屏障动作。

原因:虽然 await() 内部使用了锁,但多个线程通过屏障后,谁先获得锁是不确定的。如果需要可靠的跨线程数据传递,应使用 volatileAtomicReference 或将合并逻辑放在屏障动作中执行(屏障动作的 happens-before 规则保证了可见性)。

7. 超时后屏障破损的传播

注意:某个线程超时返回后,屏障进入破损状态,其他所有线程(包括未超时的)都会抛出 BrokenBarrierException

原因:与中断类似,超时意味着该线程无法继续等待,破坏了屏障完整性,因此必须整体失效。

8. 性能考虑:避免大量创建 CyclicBarrier 实例

注意:若需要多轮同步,尽量重用同一个 CyclicBarrier,而不是每轮都 new 一个新实例。

原因:每个 CyclicBarrier 内部包含 ReentrantLockCondition,关联了同步队列和条件队列。频繁创建销毁会增加 GC 压力和内存开销。重用并通过 reset()(需小心使用)或自然触发下一代可提升性能。


以上内容基于 JDK 8 源码openjdk-8u40)进行分析和验证,涵盖了核心方法、特性原理、时序图、场景示例、吞吐量分析和注意事项,帮助读者全面掌握 CyclicBarrier 的使用与原理。