简洁易懂的CyclicBarrier源码剖析

59 阅读4分钟

前言

此前我们已经学习了JMM,JDK与JVM层面提供的锁机制,AQS实现原理:

今天我们继续学习locks包下的一个强大的组件:CyclicBarrier。这个类会特殊一些,不像CountDownLatch,ReentrantLock的sync直接继承AQS就完事,我们一起来看一下它的实现源码。

CyclicBarrier:可循环屏障

作用:让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。 CyclicBarrier可以用于多线程计算数据,最后合并计算结果等应用场景。

为了便于讲解,我们定义当指定线程数量到达屏障时,线程唤醒继续执行的这个事件叫做:「屏障释放」

说他是循环的原因是:当触发「屏障释放」时,会让屏障恢复到最初的状态,即便出现异常也可以通过手动调用reset方法重复使用,而CountDownLatch就没有这个功能。

快速使用

CyclicBarrier barrier = new CyclicBarrier(1, () -> {
    System.out.println("触发barrierCommand");
});
new Thread(()->{
    System.out.println("线程到达屏障");
    // -------------省略异常处理
        barrier.await();
    // ------------
    System.out.println("线程离开屏障");
}).start();

输出:

线程到达屏障
触发barrierCommand
线程离开屏障

可以看到,上述例子只调用了一个await方法,这也是CyclicBarrier最核心的方法

await:到达屏障

await方法代表:当前线程已经到达该CyclicBarrier,阻塞,等待触发「屏障释放」事件后继续执行

为了避免误会还是说一下:如果调用await方法的线程刚好是最后一个到达CyclicBarrier的线程,即触发「屏障释放」事件,是不会阻塞的,该线程会负责执行barrierCommand方法,唤醒其余在屏障阻塞的线程,为CyclicBarrier的下次工作做好准备,然后继续该线程的工作。

实现原理

CyclicBarrier会特殊一些,不像之前继承AQS就完事,它基于ReentrantLock,Condition实现

核心字段
// 到达屏障,await需要获得锁
private final ReentrantLock lock = new ReentrantLock();
// 阻塞/唤醒的工具
private final Condition trip = lock.newCondition();
// 触发「屏障释放」的线程数量阈值
private final int parties;
// 触发「屏障释放」时,先执行barrierCommand方法
private final Runnable barrierCommand;
// Generation是个静态内部类 代表一个布尔值 boolean broken = false;即表示「屏障是否破损」
private Generation generation = new Generation();
// 还需要等待来到屏障的线程数量
private int count;

核心方法:await

可以说使用CyclicBarrier主要都是在调用await方法

private int dowait(boolean timed, long nanos) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        // 仍需等待的线程数-- 
        int index = --count;
        // 如果 所有线程都已经到达屏障
        if (index == 0) { 
            // 1.首先执行barrierCommand方法
            // 2.唤醒所有阻塞线程(按顺序依次唤醒)
            // 3.重置屏障用于下次使用,具体是:
                    // 3.1 count = parties;
                    // 3.2 generation = new Generation();
        }
        // 否则 根据预期的时间阻塞,或是一直阻塞下去
    } finally {
        lock.unlock();
    }
}
barrierCommand:屏障释放事件

barrierCommand是个Runnable,但它并没有通过Thread.start方法调用,也就是说barrierCommand只是个封装的线程体,并没有作为线程单独执行,源码如下:

    final Runnable command = barrierCommand;
    if (command != null)
        command.run();
        trip.signalAll();
        // set up next generation
        count = parties;
        generation = new Generation();

因此,是barrierCommand执行完毕,才去执行唤醒操作,这是有先后顺序的

屏障失效的场景

1、线程中断

一旦调用await方法的线程被interrupt,就会触发breakBarrier方法

2、等待超时

CyclicBarrier允许指定等待超时时间await(long timeout, TimeUnit unit),一旦超出时间仍未触发 「屏障释放」,就会触发breakBarrier方法

而breakBarrier方法,会将所有阻塞的线程依次唤醒,并设置broken标识为true,源码如下:

private void breakBarrier() {
    generation.broken = true;
    count = parties;
    trip.signalAll();
}

这些被唤醒的线程,以及正在对一个损坏的屏障调用await方法的线程,会抛出「BrokenBarrierException」

而触发超时的线程会抛出「TimeoutException」,注意区分

修复屏障

CyclicBarrier提供了修复屏障的方法,下面两个API可以搭配使用

    // 由于篇幅问题 省略加解锁逻辑 下面这两个方法都要先获取锁
    public boolean isBroken() {
            return generation.broken;
    }
    public void reset() {
            breakBarrier();   // 不理解这里breakBarrier有什么用,欢迎在评论区告知我
            nextGeneration(); // start a new generation
    }

会依次唤醒所有之前await的线程,重置count与generation,为下一次工作做好准备。

小结

CyclicBarrier的源码实现并不复杂,并且功能强大,应用广泛,非常推荐想读源码但又担心源码过于复杂的同学迈出阅读源码的第一步。