什么是CyclicBarrier
上一篇文章中介绍完了Condition条件,这篇文章就介绍一下使用到了Condition了的CyclicBarrier同步辅助类。CyclicBarrier翻译过来可以理解成“关卡”、“屏障”。想象这样一个场景:100米赛跑比赛上,有4名运动员参见比赛,比较公平的赛法是——等待大家都做好起跑动作,准备好时,裁判员再发信号枪,然后大家一起跑。而在多线程的场景下,每一个运动员都一个线程,而裁判员就是这个CyclicBarrier。
当然这里要注意要将CyclicBarrier与之后要介绍的CountDownLatch区分开来,这两个还是很好区分的,这个待介绍CountDownLatch的时候进行区分。
废话不多说,我们先用一个小demo看看CyclicBarrier到底做什么用的
CyclicBarrier的使用示例
为了理解方便,那我就很简单的用打印的方式模拟一下上面介绍的时候说到的场景:
/**
* Created by fei on 2017/6/7.
*/
public class CyclicBarrierDemo {
public static final int INIT_SIZE = 4;
private static CyclicBarrier barrier;
public static void main(String[] args) {
System.out.println("开启CyclicBarrier屏障(裁判员就位)");
//初始化CyclicBarrier
barrier = new CyclicBarrier(INIT_SIZE, new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName()+"线程通知:所有线程都已经准备好了,CyclicBarrier屏障去除(所有运动员都准备完毕,发信号枪)");
}
});
//开启4个线程,充当运动员
for (int i=0;i<INIT_SIZE;i++){
new ThreadDemo().start();
}
}
static class ThreadDemo extends Thread {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName()+"线程准备好了,等待CyclicBarrier屏障去除(一名运动员准备好了)");
barrier.await();
System.out.println(Thread.currentThread().getName()+"线程继续运行(开始跑)");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
}
某次运行结果:
开启CyclicBarrier屏障(裁判员就位)
Thread-0线程准备好了,等待CyclicBarrier屏障去除(一名运动员准备好了)
Thread-1线程准备好了,等待CyclicBarrier屏障去除(一名运动员准备好了)
Thread-3线程准备好了,等待CyclicBarrier屏障去除(一名运动员准备好了)
Thread-2线程准备好了,等待CyclicBarrier屏障去除(一名运动员准备好了)
Thread-2线程通知:所有线程都已经准备好了,CyclicBarrier屏障去除(所有运动员都准备完毕,发信号枪)
Thread-2线程继续运行(开始跑)
Thread-1线程继续运行(开始跑)
Thread-0线程继续运行(开始跑)
Thread-3线程继续运行(开始跑)
根据上面这个简单的小示例,应该不难看出CyclicBarrier的意思。再上一张网上盗的图加强理解:
而CyclicBarrier到底是怎么实现的呢?接下来我们就简单分析一下CyclicBarrier的源码。
CyclicBarrier源码解析
先看一下CyclicBarrier中成员变量的组成:
可以看出,CyclicBarrier是由ReentrantLock和Condition来实现的。具体每个变量都有什么意义,我们在分析源码的时候具体说。
我们主要从CyclicBarrier的构造方法和它的await方法分析说起。
CyclicBarrier构造函数
CyclicBarrier有两个构造函数:
//带Runnable参数的函数
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;//有几个运动员要参赛
this.count = parties;//目前还需要几个运动员准备好
//你要在所有线程都继续执行下去之前要执行什么操作,可以为空
this.barrierCommand = barrierAction;
}
//不带Runnable参数的函数
public CyclicBarrier(int parties) {
this(parties, null);
}
其中,第二个构造函数调用的是第一个构造函数,这个 Runnable barrierAction 参数是什么呢?其实在上面的小示例中我们就用到了这个Runnable参数,它就是在所有线程都准备好之后,满足Barrier条件时,并且在所有线程继续执行之前,我们可以执行这个Runnable。但是值得注意的是,这不是新起了一个线程,而是通过最后一个准备好的(也就是最后一个到达Barrier的)线程承担启动的。这一点我们在上面示例中打印的运行结果中也可以看出来:Thread-2线程是最后一个准备好的,就是它执行的这个barrierAction。
这里parties和count不要混淆,parties是表示必须有几个线程要到达Barrier,而count是表示目前还有几个线程未到达Barrier。也就是说,只有当count参数为0时,Barrier条件即满足,所有线程可以继续执行。
count变量是怎么减少到0的呢?是通过Barrier执行的await方法。下面我们就看一下await方法。
await方法
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
await方法调用的dowait方法:
/**
* Main barrier code, covering the various policies.
*/
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock(); //获取ReentrantLock互斥锁
try {
final Generation g = generation;//获取generation对象
if (g.broken)//如果generation损坏,抛出异常
throw new BrokenBarrierException();
if (Thread.interrupted()) {
//如果当前线程被中断,则调用breakBarrier方法,停止CyclicBarrier,并唤醒所有线程
breakBarrier();
throw new InterruptedException();
}
int index = --count; // 看到这里了吧,count减1
//index=0,也就是说,有0个线程未满足CyclicBarrier条件,也就是条件满足,
//可以唤醒所有的线程了
if (index == 0) { // tripped
boolean ranAction = false;
try {
//这就是构造器的第二个参数,如果不为空的话,就执行这个Runnable的run方法,
//你看,这里是执行的是run方法,也就是说,并没有新起一个另外的线程,
//而是最后一个执行await操作的线程执行的这个run方法。
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
//这个nextGeneration下面会说,就是唤醒了所有的等待线程,
//并且更新了generation
nextGeneration();
return 0;
} finally {
if (!ranAction)//保证一定能唤醒所有线程
breakBarrier();
}
}
// loop until tripped, broken, interrupted, or timed out
//上面是源代码中的注释,说的很清楚了,
//当barrier的条件满足、当前线程被中断或者已经超时才会跳出循环
for (;;) {
try {
if (!timed)//如果没有指定超时参数,则直接调用Condition的await方法。
trip.await();
else if (nanos > 0L)//否则根据超时时间调用awaitNanos方法
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
//执行过程中,线程被中断的话,看一下generation是否有该改变,并且是否已经破坏
if (g == generation && ! g.broken) {
breakBarrier();//执行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();
}
}
if (g.broken)//如果当前generation已经损坏,抛出异常
throw new BrokenBarrierException();
if (g != generation)//如果generation已经更新换代,则返回index
return index;
//如果是参数是超时等待,并且已经超时,则执行breakBarrier()方法
//唤醒所有等待线程。
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
简单来说,如果不发生异常,线程不被中断,那么dowait方法会调用Condition的await方法(具体Condition的原理请看前面的文章),直到所有线程都准备好,即都执行了dowait方法,(做count的减操作,直到count=0),即CyclicBarrier条件已满足,就会执行唤醒线程操作,也就是上面的nextGeneration()方法。可能大家会有疑惑,这个Generation是什么东西呢?其实这个Generation定义的很简单,就一个布尔值的成员变量:
private Generation generation = new Generation();
private static class Generation {
boolean broken = false;
}
Generation 可以理解成“代”,我们要知道,CyclicBarrier是可以重复使用的,CyclicBarrier中的同一批线程属于同一“代”,当所有线程都满足了CyclicBarrier条件,执行唤醒操作nextGeneration()方法时,会新new 出一个Generation,代表一下“代”。
nextGeneration的源码
private void nextGeneration() {
// signal completion of last generation
trip.signalAll();//调用Condition的signalAll方法,唤醒所有await的线程
// set up next generation
count = parties;//重置count值
//生成新的Generation,表示上一代的所有线程已经唤醒,进行更新换代
generation = new Generation();
}
breakBarrier源码
再来看一下breakBarrier的代码,breakBarrier方法是在当前线程被中断时执行的,用来唤醒所有的等待线程:
private void breakBarrier() {
generation.broken = true;//表示当代因为线程被中断,已经发成损坏了
count = parties;//重置count值
trip.signalAll();//调用Condition的signalAll方法,唤醒所有await的线程
}