什么是CyclicBarrier?

CyclicBarrie和上一篇中讲到CountDownLatch很类似,它能阻塞一组线程直到某个事件的发生。

栅栏与闭锁的关键区别在于:所有必须同时到达栅栏位置才能够继续执行。也就是闭锁用于等待某个事件,栅栏用于等待其它线程

CyclicBarrier的基本过程

CyclicBarrier可以使一定数量的线程反复的在栅栏处汇集。

  1. 当线程到达栅栏位置时将调用await方法,直到所有方法都到达栅栏位置
  2. 当所有线程都到达栅栏位置后,那么栅栏将打开,所有的线程将被释放
  3. 栅栏被释放后会执行barrierAction的runable,然后重置计数器

CyclicBarrier应用示例

import java.util.concurrent.CyclicBarrier;

/**
 * 同学们去春游
 *
 * 首先:同学们都先上公司门口的大巴。人齐了之后,巴士出发。
 * 其次:所有巴士都到达景点后,大家集合,开始春游。
 */
public class CyclicBarrierDemo {

    private static final int NUMS = 5;

    public static final CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMS, new Master());

    public static void main(String[] args) {
        for (int i = 0; i < NUMS; i++) {
            Thread thread = new Thread(new Student(i, cyclicBarrier));
            thread.start();
        }
    }

}

class Student implements Runnable {

    private CyclicBarrier cyclicBarrier;

    private volatile Integer studenNo = 0;

    public Student(Integer studenNo, CyclicBarrier cyclicBarrier) {
        this.studenNo = studenNo;
        this.cyclicBarrier = cyclicBarrier;
    }

    @Override
    public void run() {
        try {
            System.out.println("学生" + studenNo + ", 已经上巴士。");
            cyclicBarrier.await();
            System.out.println("学生" + studenNo + ", 巴士已经到达目的地。");
            cyclicBarrier.await();
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
}

class Master implements Runnable {

    private static int step = 1;

    @Override
    public void run() {
        if (step == 1) {
            System.out.println("同学们都已经上大巴了,咱们出发!");
        } else if (step == 2) {
            System.out.println("所有大巴都到了,同学们开始春游!");
        }
        step++;
    }
}

示例中所有线程会在栅栏中集结两次,一次是所有同学上大巴;第二次是所有大巴都到达目的地。

CyclicBarrier源码解析

从上面的示例,主要有如下关键方法:

  1. 构造方法:CyclicBarrier(NUMS, new Master());
  2. 阻塞方法:cyclicBarrier.await();

构造方法:CyclicBarrier(NUMS, new Master())

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);
    }

CyclicBarrier有两个构造函数,其中:

  • parties:表示屏障拦截的线程数量,例子中为5
  • barrierAction:表示在达到拦截的线程数量后执行barrierAction,然后恢复阻塞的线程执行

阻塞方法:cyclicBarrier.await()

public int await() throws InterruptedException, BrokenBarrierException {
        try {
            // 不超时等待
            return dowait(false, 0L);
        } catch (TimeoutException toe) {
            throw new Error(toe); // cannot happen
        }
    }
    
    public int await(long timeout, TimeUnit unit)
            throws InterruptedException,
            BrokenBarrierException,
            TimeoutException {
        return dowait(true, unit.toNanos(timeout));
    }

两个方法的区别在于第二个可以传入超时参数,默认是不超时,它们都调用dowait方法,代码如下:

private int dowait(boolean timed, long nanos)
            throws InterruptedException, BrokenBarrierException,
            TimeoutException {
        // 使用独占锁来执行dowait方法,并发性可能不是很高
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            // 当前代
            final Generation g = generation;
            // 如果当前代损坏了则抛出异常
            if (g.broken)
                throw new BrokenBarrierException();

            // 如果线程中断则抛出异常
            if (Thread.interrupted()) {
                // 将损坏状态设置为true,并通知其他阻塞在此栅栏上的线程
                breakBarrier();
                throw new InterruptedException();
            }

            // 获取下标
            int index = --count;
            // 如果是 0,说明最后一个线程调用了该方法
            if (index == 0) {  // tripped
                boolean ranAction = false;
                try {
                    final Runnable command = barrierCommand;
                    // 执行栅栏任务
                    if (command != null)
                        command.run();
                    ranAction = true;
                    // 更新一代,将count重置,将generation重置
                    nextGeneration();
                    return 0;
                } finally {
                    // 如果执行栅栏任务的时候失败了,就将损坏状态设置为true
                    if (!ranAction)
                        breakBarrier();
                }
            }

            // 自旋直到触发、broken、中断或超时
            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();
                    }
                }

                // 当有任何一个线程中断了,就会调用breakBarrier方法,就会唤醒其他的线程,其他线程醒来后,也要抛出异常
                if (g.broken)
                    throw new BrokenBarrierException();

                // g != generation表示正常换代了,返回当前线程所在栅栏的下标
                // 如果 g == generation,说明还没有换代,那为什么会醒了?
                // 因为一个线程可以使用多个栅栏,当别的栅栏唤醒了这个线程,就会走到这里,所以需要判断是否是当前代。
                // 正是因为这个原因,才需要generation来保证正确。
                if (g != generation)
                    return index;

                // 如果有时间限制,且时间小于等于0,销毁栅栏并抛出超时异常
                if (timed && nanos <= 0L) {
                    breakBarrier();
                    throw new TimeoutException();
                }
            }
        } finally {
            lock.unlock();
        }
    }

基本流程如下:

  1. 执行一些验证:栅栏是否broken、线程是否中断
  2. 如果是最后一个线程调用dowait,则执行栅栏任务barrierAction,然后更新代nextGeneration
private void nextGeneration() {
        // 唤醒所有阻塞线程
        trip.signalAll();
        // 重置count
        count = parties;
        // 重新生成下一代
        generation = new Generation();
    }
  1. 如果不是最后一个线程调用dowait,则自旋,trip.await()会进行阻塞,直至发生如下情况才会被唤醒或终止:
  • 最后一个线程到达,即index==0
  • 某个参与栅栏的线程等待超时
  • 某个参与栅栏的线程被中断
  • 调用了CyclicBarrier的reset()方法,该方法会将屏障置为初始状态
  1. 在被唤醒之后,栅栏没有损坏且是同一代,则返回下标index

在barrier损坏或有一个线程被中断,会调用breakBarrier方法来终止所有线程

private void breakBarrier() {
        // 设置broken为true,自旋的线程会抛出BrokenBarrierException异常
        generation.broken = true;
        count = parties;
        // 唤醒所有线程,由于broken为true,所有都会被终止并抛出异常
        trip.signalAll();
    }

至此,await()方法源码全部解析完毕

CyclicBarrier和CountDownLatch的区别

  • CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置或自动重置,可以使用多次,所以CyclicBarrier能够处理更为复杂的场景;
  • CyclicBarrier还提供了一些其他有用的方法,比如getNumberWaiting()方法可以获得CyclicBarrier阻塞的线程数量,isBroken()方法用来了解阻塞的线程是否被中断;
  • CountDownLatch允许一个或多个线程等待一组事件的产生,而CyclicBarrier用于等待其他线程运行到栅栏位置

思考

CyclicBarrier有没有什么不足?

有,在dowait方法中,我们可以发现它调用的是ReentrantLock独占锁的方式来实现多线程并发,在并发量大的情况下性能可能不是很高

为什么要有代的区分?

因为线程在等待唤醒的过程中,如果线程被其它的栅栏唤醒,但不是同一个栅栏,也就是不同一个代,可以通过来判断是不是同一个代,然后区分是正常结束返回下标还是继续自旋等待

在换代nextGeneration的过程中,如果某个线程中断会怎么样?

不影响,正常结束。JDK中认为任务已完成,就不会在乎中断,但是会打个中断标记,在dowait方法中有注释
java Thread.currentThread().interrupt();