JUC学习之循环栅栏CyclicBarrier

爱⌒轻易说出口 提交于 2020-02-29 10:04:14

一、简介

前面已经简单介绍了CountDownLatch闭锁,本文的CyclicBarrier其实跟闭锁差不多,当然还是存在一些区别。

官网介绍如下:

CyclicBarrier是一种同步辅助工具,允许一组线程彼此等待到达一个共同的障碍点。CyclicBarrier在包含固定大小的线程的程序中非常有用,这些线程有时必须彼此等待。这个屏障被称为循环屏障,因为它可以在等待的线程被释放后重新使用。

CyclicBarrier支持一个可选的Runnable命令,该命令在在最后一个线程到达之后,但是在释放任何线程之前执行,这个屏障动作对于在任何一方继续之前更新共享状态非常有用。

通俗理解,就是CyclicBarrier可以使一定数量的线程反复地在栅栏位置处汇集。在CyclicBarrier类的内部有一个计数器,每个线程在到达屏障点的时候都会调用await方法将自己阻塞,此时计数器会减1,当计数器减为0的时候,所有因调用await方法而被阻塞的线程将被唤醒。如果所有线程都到达栅栏位置,那么栅栏将打开,此时所有的线程都将被释放,而栅栏将被重置以便下次使用(栅栏可重复使用)。

二、常见API

  • 构造方法:
//创建一个新的CyclicBarrier,当给定数量的参与方(线程)正在等待它时,它将会跳闸,并且在跳闸时不会执行预定义的操作。
CyclicBarrier(int parties)
//创建一个新的CyclicBarrier,当给定数量的参与方(线程)正在等待它时,它将跳闸,当屏障跳闸时,它将执行给定的barrier动作,由最后一个进入屏障的线程执行。
CyclicBarrier(int parties, Runnable barrierAction)
  • 常见方法:

int

await()

等待,直到所有参与方都已调用此屏障上的等待。

int

await(long timeout, TimeUnit unit)

等待,直到所有参与方都已调用此屏障上的wait,或指定的等待时间已过。

int

getNumberWaiting()

返回当前在屏障处等待的参与方的数量

int

getParties()

返回需要跨越此屏障的参与方的数量。

boolean

isBroken()

查询此屏障是否处于中断状态。

void

reset()

将屏障重置为其初始状态

二、使用

下面我们通过一个简单的例子说明CyclicBarrier如何使用。

示例:模拟我们去聚会吃饭时要等所有人都都齐了才开始吃饭

public class T03_CyclicBarrier {
    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(10);
        for (int i = 1; i <= 10; i++) {
            System.out.println("A" + i + "开始等待其他人...");
            new Thread(new Person(cyclicBarrier), "A" + i).start();
        }
    }
}

class Person implements Runnable {
    /**
     * 循环栅栏
     */
    private CyclicBarrier cyclicBarrier;

    public Person(CyclicBarrier cyclicBarrier) {
        this.cyclicBarrier = cyclicBarrier;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "正在等待其他人到来....");
        //线程阻塞
        try {
            cyclicBarrier.await();
        } catch (InterruptedException | BrokenBarrierException e) {
            e.printStackTrace();
        }

        //模拟吃饭操作
        System.out.println(Thread.currentThread().getName() + "开始吃饭....");
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "吃完饭了....");
    }
}

运行结果:

A1开始等待其他人...
A2开始等待其他人...
A3开始等待其他人...
A4开始等待其他人...
A5开始等待其他人...
A6开始等待其他人...
A7开始等待其他人...
A8开始等待其他人...
A9开始等待其他人...
A10开始等待其他人...
A1正在等待其他人到来....
A3正在等待其他人到来....
A5正在等待其他人到来....
A4正在等待其他人到来....
A2正在等待其他人到来....
A6正在等待其他人到来....
A7正在等待其他人到来....
A8正在等待其他人到来....
A9正在等待其他人到来....
A10正在等待其他人到来....
A10开始吃饭....
A1开始吃饭....
A2开始吃饭....
A4开始吃饭....
A6开始吃饭....
A7开始吃饭....
A5开始吃饭....
A3开始吃饭....
A8开始吃饭....
A9开始吃饭....
A8吃完饭了....
A1吃完饭了....
A3吃完饭了....
A9吃完饭了....
A10吃完饭了....
A6吃完饭了....
A2吃完饭了....
A7吃完饭了....
A5吃完饭了....
A4吃完饭了....

由执行结果看,只有10个人都到来,所有人即10个线程才开始执行“开始吃饭”操作。

下面我们测试一下使用第二种方式创建CyclicBarrier:

CyclicBarrier cyclicBarrier = new CyclicBarrier(10,()->{
    System.out.println("===========所有人都完成等待,准备吃饭===========");
});

运行结果:

A1开始等待其他人...
A2开始等待其他人...
A3开始等待其他人...
A4开始等待其他人...
A5开始等待其他人...
A6开始等待其他人...
A7开始等待其他人...
A1正在等待其他人到来....
A8开始等待其他人...
A9开始等待其他人...
A10开始等待其他人...
A2正在等待其他人到来....
A6正在等待其他人到来....
A7正在等待其他人到来....
A3正在等待其他人到来....
A4正在等待其他人到来....
A5正在等待其他人到来....
A8正在等待其他人到来....
A9正在等待其他人到来....
A10正在等待其他人到来....
===========所有人都完成等待,准备吃饭===========
A10开始吃饭....
A1开始吃饭....
A2开始吃饭....
A6开始吃饭....
A7开始吃饭....
A3开始吃饭....
A4开始吃饭....
A5开始吃饭....
A8开始吃饭....
A9开始吃饭....
A8吃完饭了....
A1吃完饭了....
A2吃完饭了....
A7吃完饭了....
A3吃完饭了....
A6吃完饭了....
A4吃完饭了....
A9吃完饭了....
A5吃完饭了....
A10吃完饭了....

可以看到,那个Runnable barrierAction感觉就像是一个回调方法,等所有线程都到达那个屏障后,各个线程还没开始执行业务时,进行一些操作。

三、源码解读

CyclicBarrier内部是基于ReentrantLock可重入锁和Condition来实现的,这两个后面也会介绍。

CyclicBarrier类主要提供了两个构造方法:

  • CyclicBarrier(int parties):parties表示屏障拦截的线程数量,每个线程使用await()方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
  • CyclicBarrier(int parties, Runnable barrierAction):当所有线程到达屏障时,优先执行barrierAction,可以进行一些额外的操作。

CyclicBarrier内部存在一个私有的静态内部类Generation:用来描述CyclicBarrier的更新换代。在CyclicBarrier中,同一批的线程属于同一代。当所有线程都到达栅栏之后,Generation 则会进行更新换代。

private static class Generation {
        //标识该当前CyclicBarrier是否已经处于中断状态
        //默认为非中断状态
        boolean broken = false;
}

属性说明:

//使用可重入锁ReentrantLock 来保护栅栏
private final ReentrantLock lock = new ReentrantLock();
//对应锁的条件
private final Condition trip = lock.newCondition();
//即线程数量
private final int parties;
//触发时要运行的命令,也是换代时执行的操作
private final Runnable barrierCommand;
//当前代
private Generation generation = new Generation();
//计数器
private int count;

下面我们看一下其中比较重要的方法 ------- await()方法

  • await()方法主要是告诉CyclicBarrier我已经到达了栅栏点,并且会阻塞当前线程,直到所有线程都到达栅栏点。
//不带超时时间的await方法
public int await() throws InterruptedException, BrokenBarrierException {
    try {
        return dowait(false, 0L);
    } catch (TimeoutException toe) {
        throw new Error(toe); // cannot happen
    }
}

//带超时时间的await方法
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 {
    //可重入锁
    final ReentrantLock lock = this.lock;
    //获取锁对象
    lock.lock();
    try {
        //当前代
        final Generation g = generation;
        //如果当前代broken标志位true,表示CyclicBarrier已损坏,抛出BrokenBarrierException异常
        if (g.broken)
            throw new BrokenBarrierException();
        
        //如果当前线程被中断,抛出InterruptedException中断异常
        if (Thread.interrupted()) {
            //将当前的屏障生成设置为被打破,并唤醒所有人。只有在持有锁时才调用。
            breakBarrier();
            throw new InterruptedException();
        }
        
        //获取下标索引
        int index = --count;
        //如果index为0,说明所有线程都到达了栅栏处
        if (index == 0) {  // tripped
            //栅栏任务执行成功与否标志
            boolean ranAction = false;
            try {
                //执行栅栏任务
                final Runnable command = barrierCommand;
                if (command != null)
                    //调用run()方法执行具体的任务
                    command.run();
                ranAction = true;
                //更新下一代
                //更新屏障状态并唤醒所有人。只有在持有锁时才调用。
                nextGeneration();
                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 {
                    // We're about to finish waiting even if we had not
                    // been interrupted, so this interrupt is deemed to
                    // "belong" to subsequent execution.
                    //给线程设置一个中断标志,线程仍会继续运行
                    Thread.currentThread().interrupt();
                }
            }

            //如果当前代broken标志位true,表示CyclicBarrier已损坏,抛出BrokenBarrierException异常
            if (g.broken)
                throw new BrokenBarrierException();
        
            //如果正常换代时执行了nextGeneration();后generation重新生成了下一代
            //如果不相等的话说明是正常更新换代,返回当前下标索引
            if (g != generation)
                return index;
            //如果超时了或者指定的限制时间<=0,那么将当前的屏障生成设置为被打破,并唤醒所有人
            if (timed && nanos <= 0L) {
                breakBarrier();
                //抛出超时异常
                throw new TimeoutException();
            }
        }
    } finally {
        //手动释放锁对象
        lock.unlock();
    }
}

可以看到,如果该线程不是最后一个调用await方法的线程,则它会一直处于等待状态,除非发生以下情况:

  • 最后一个线程到达,即index == 0;
  • 某个参与线程等待超时;
  • 某个参与线程被中断;
  • 调用了CyclicBarrier的reset()方法,该方法会将屏障重置为初始状态;

 

  • 更新换代的方法nextGeneration():触发时机就是当所有线程都已经到达栅栏处(即index == 0)
//更新栅栏的状态并且唤醒所有其他等待的线程
private void nextGeneration() {
    // signal completion of last generation
    trip.signalAll();
    // 建立下一代,重新初始化计数器的值
    count = parties;
    //重新生成Generation对象
    generation = new Generation();
}
  • 销毁CyclicBarrier的方法breakBarrier():
private void breakBarrier() {
    //将标志修改为true
    generation.broken = true;
    // 建立下一代,重新初始化计数器的值
    count = parties;
    //唤醒所有其他等待的线程
    trip.signalAll();
}
  • 其他的一些方法说明:
//返回需要跨越此屏障的参与方的数量。
public int getParties() {
    return parties;
}

//查询此屏障是否处于中断状态。
public boolean isBroken() {
    //通过独占锁来实现,防止其他线程修改broken标志
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return generation.broken;
    } finally {
        lock.unlock();
    }
}

//重置CyclicBarrier的状态
public void reset() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        breakBarrier();   // break the current generation
        nextGeneration(); // start a new generation
    } finally {
        lock.unlock();
    }
}

//返回当前在屏障处等待的参与方的数量
public int getNumberWaiting() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return parties - count;
    } finally {
        lock.unlock();
    }
}

四、总结

下面将CyclicBarrier和CountDownLatch进行一下比较,区别如下:

  • CountDownLatch的计数器只能使用一次,而CyclicBarrier可以重复使用,所以CyclicBarrier能够处理更为复杂的场景;
  • CyclicBarrier还提供了一些其他有用的API,在上面源码的时候已经介绍。比如getNumberWaiting()、isBroken()方法等待;
  • CountDownLatch用于一个或多个线程等待一组事件的产生,而CyclicBarrier用于等待其他线程运行到栅栏位置;
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!