前言
在前几篇文章中,
聊聊并发:(九)concurrent包之ReentrantLock分析
聊聊并发:(十一)concurrent包之Condition源码分析
我们对concurrent包中的locks下的几种锁的源码实现进行了分析,了解了它们的实现原理,在开发高并发的程序中,深入理解锁的使用是非常有必要的,如果没有读过前几篇的朋友,欢迎阅读。
本篇,我们继续分析concurrent包中的CyclicBarrier,CyclicBarrier是并发程序中经常用到的辅助类,可以帮助我们更好的编写并发程序,我们来一起了解一下它的使用方式以及实现机制。
CyclicBarrier的实现机制是依赖于ReentrantLock于Condition实现的,如果您对这两个类不了解,建议先阅读之前的文章关于这两个类的介绍。
CyclicBarrier介绍
CyclicBarrier是一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时CyclicBarrier很有用。它的功能与Thread中的join()非常的相似,不过它的功能会更加的强大。
我们举一个生活中的例子:
在奥运会百米赛跑中,假设一共有10个跑道,分别有十个运动员,必须等待十个运动员全部在跑道准备就位,裁判员才可以开枪,任何一个运动员没有准备好之前,其他运动员都需要等待,这个就是CyclicBarrier的作用。
CyclicBarrier构造方法如下:
CyclicBarrier(int parties)
创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,但它不会在启动 barrier 时执行预定义的操作。
------------------------------------------------------------------------------
CyclicBarrier(int parties, Runnable barrierAction)
创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,并在启动 barrier 时执行给定的屏障操作,该操作由最后一个进入 barrier 的线程执行。
------------------------------------------------------------------------------
CyclicBarrier的方法列表如下:
int await()
在所有参与者都已经在此 barrier 上调用 await 方法之前,将一直等待。
------------------------------------------------------------------------------
int await(long timeout, TimeUnit unit)
在所有参与者都已经在此屏障上调用 await 方法之前将一直等待,或者超出了指定的等待时间。
------------------------------------------------------------------------------
int getNumberWaiting()
返回当前在屏障处等待的参与者数目。
------------------------------------------------------------------------------
int getParties()
返回要求启动此 barrier 的参与者数目。
------------------------------------------------------------------------------
boolean isBroken()
查询此屏障是否处于损坏状态。
------------------------------------------------------------------------------
void reset()
将屏障重置为其初始状态。
------------------------------------------------------------------------------
CyclicBarrier的构造方法,支持传入一个整形参数,代表执行线程的个数,每当一个线程执行await()方法后,该数字会减一,直至到0前,其他线程会一直等待。
CyclicBarrier使用示例
我们先来看一下CyclicBarrier是如何使用的:
public class CyclicBarrierDemo {
public static void main(String[] args) throws Exception {
CyclicBarrier cyclicBarrier = new CyclicBarrier(6);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
System.out.println("当前线程:" + Thread.currentThread().getName() + ", 等待其他线程准备就绪");
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
cyclicBarrier.await();
System.out.println("全部线程就绪,开始执行");
}
}
输出结果:
当前线程:Thread-0, 等待其他线程准备就绪
当前线程:Thread-3, 等待其他线程准备就绪
当前线程:Thread-1, 等待其他线程准备就绪
当前线程:Thread-4, 等待其他线程准备就绪
当前线程:Thread-2, 等待其他线程准备就绪
全部线程就绪,开始执行
这个示例非常的简单,我们创建了一个CyclicBarrier,设置其大小为5,然后新建5个线程,在线程方法中,执行await()操作,每次一个线程执行await(),计数器就会减一,直到减到0之前,其他线程都会等待;
根据结果我们也可以看到,当全部5个线程都执行完毕之后,才输出了"全部线程就绪,开始执行"。
CyclicBarrier源码实现
CyclicBarrier的实现依赖于ReentrantLock与Condition,对于这两个类的功能,前面的文章中我们已经详细介绍过,我们先看一下CyclicBarrier的大体结构:
public class CyclicBarrier {
private static class Generation {
boolean broken = false;
}
/** The lock for guarding barrier entry */
private final ReentrantLock lock = new ReentrantLock();
/** Condition to wait on until tripped */
private final Condition trip = lock.newCondition();
/** The number of parties */
private final int parties;
/* The command to run when tripped */
private final Runnable barrierCommand;
/** The current generation */
private Generation generation = new Generation();
private int count;
....
}
上面就是CyclicBarrier中几个主要的变量,基本上大家通过注释可以看懂是干嘛的,其中需要提的是Generation这个内部类,它的作用是每一个barrier代表了一个Generation的实例,Generation类有一个属性broken,用来表示当前barrier是否被损坏,Generation的状态对于CyclicBarrier的控制是非常重要的,后面我们会提到。
构造方法:
public CyclicBarrier(int parties, Runnable barrierAction) {
//线程数目小与等于0,抛出异常
if (parties <= 0) {
throw new IllegalArgumentException();
}
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
构造方法比较简单,可以指定关联该CyclicBarrier的线程数量,并且可以指定在所有线程都进入屏障后的执行动作,该执行动作由最后一个进行屏障的线程执行。
如果不指定Runnable对象,即不进行任何操作。
await()
await()是CyclicBarrier最主要的一个方法,其作用我们在上面已经提过了,现在我们看一下其源码实现:
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
核心是调用了dowait()方法,我们继续看:
/**
* Main barrier code, covering the various policies.
*/
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
//1、获取锁
lock.lock();
try {
//2、保存当前代
final Generation g = generation;
if (g.broken)
throw new BrokenBarrierException();
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
//3、计数器自减
int index = --count;
//4、当计数器为0时,结束流程
if (index == 0) { // tripped
boolean ranAction = false;
try {
//5、获取结束时执行动作
final Runnable command = barrierCommand;
//如果动作不为空,执行
if (command != null)
command.run();
ranAction = true;
//6、重置当前代
nextGeneration();
return 0;
} finally {
//7、未执行任何动作,破坏掉栅栏
if (!ranAction)
breakBarrier();
}
}
// loop until tripped, broken, interrupted, or timed out
// 进行死循环,直到被破坏、打断、超时,结束循环
for (;;) {
try {
//8、如果未设置超时,当前线程进入Condition的等待队列
if (!timed)
trip.await();
//如果设置了超时时间,当前线程在超时时间之前,进入等待队列等待
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
//9、如果出现打断异常,判断保存的代等于当前代并且屏障没有被损坏
if (g == generation && ! g.broken) {
//10、破坏掉栅栏
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();
}
}
//11、如果保存的代被破坏,抛出异常
if (g.broken)
throw new BrokenBarrierException();
//12、如果保存的代不等于当前代,返回index
if (g != generation)
return index;
//13、如果设置了等待时间,并且等待时间小于0,破坏栅栏,并抛出异常
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
//14、释放锁资源
lock.unlock();
}
}
上面就是dowait()方法的实现,具体流程请参见注释,整体流程并不复杂,其中依赖于ReentrantLock与Condition类,这两个类的用法与实现机制我们在前面的文章中介绍过,不了解的读者可以点击这里。
在dowait()方法中,调用了两个方法,分别是nextGeneration()与breakBarrier(),这两个方法非常的重要,我们来看一下其实现:
nextGeneration():
private void nextGeneration() {
// signal completion of last generation
trip.signalAll();
// set up next generation
count = parties;
generation = new Generation();
}
nextGeneration()主要的作用是唤醒在当前Condition对象的等待队列中等待的全部线程,并更新当前代,代的概念这里我们说一下,它是CyclicBarrier中的一个内部类,它的作用更像是一个标志位的作用,其只有一个属性,broken,记录当前代释是否被破坏。
nextGeneration()会在全部线程进入屏障后会被调用,即生成下一个代,使得全部线程又可以重新进入到栅栏中,从这里可以得知,CyclicBarrier的栅栏是可以多次复用的,而这个特性与另一个功能相似的类CountDownLatch有所不同,后面的篇幅中我们会分析CountDownLatch。
我们再看一下另一个方法,breakBarrier():
private void breakBarrier() {
generation.broken = true;
count = parties;
trip.signalAll();
}
breakBarrier()方法比较简单,将当前代的破坏状态设置为true,并唤醒在当前Condition对象的等待队列中等待的全部线程。
OK,dowait()方法的核心实现我们已经看完了,我们用一张图来描述下它的工作流程:
OK,await()方法我们就介绍在这么多,接下来我们看一下reset()方法的实现:
reset()
public void reset() {
final ReentrantLock lock = this.lock;
//1、获取锁
lock.lock();
try {
// 2、破坏当前代
breakBarrier();
// 3、创建新的代
nextGeneration();
} finally {
lock.unlock();
}
}
reset()方法的实现如上,可以看到比较简单,首先获取锁,然后破坏掉当前代,并创建新的代。
复用性
前面我们提到过,CyclicBarrier是可以复用的,那么这个应该如何理解呢?我们用一个小Demo来演示一下:
public class CyclicBarrierDemo {
public static void main(String[] args) throws BrokenBarrierException, InterruptedException {
CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
System.out.println("线程准备进入等待,当前线程:" + Thread.currentThread().getName());
cyclicBarrier.await();
System.out.println("全部线程就位,第一轮结束");
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
System.out.println("线程准备进入等待,当前线程:" + Thread.currentThread().getName());
cyclicBarrier.await();
System.out.println("全部线程就位,第二轮结束");
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}
输出结果:
线程准备进入等待,当前线程:Thread-1
线程准备进入等待,当前线程:Thread-0
线程准备进入等待,当前线程:Thread-2
线程准备进入等待,当前线程:Thread-3
线程准备进入等待,当前线程:Thread-4
全部线程就位,第一轮结束
全部线程就位,第一轮结束
全部线程就位,第一轮结束
全部线程就位,第一轮结束
全部线程就位,第一轮结束
线程准备进入等待,当前线程:Thread-5
线程准备进入等待,当前线程:Thread-6
线程准备进入等待,当前线程:Thread-9
线程准备进入等待,当前线程:Thread-7
线程准备进入等待,当前线程:Thread-8
全部线程就位,第二轮结束
全部线程就位,第二轮结束
全部线程就位,第二轮结束
全部线程就位,第二轮结束
全部线程就位,第二轮结束
在这个示例中(比较low),我们创建了一个CyclicBarrier,初始化大小设置为5,然后依次启动两组线程,每组线程大小为5,在线程中执行await(),从执行结果中我们可以看出,第一轮线程组执行完成后,在执行第二轮的时候,CyclicBarrier进行了重新初始化。
其机制我们在源码中已经提到过了,是因为当计数器为0的时候,会调用nextGeneration()方法,初始化下一个代,因此,CyclicBarrier是可以复用的。
使用场景
CyclicBarrier可以用于多个线程执行任务,需要等待多个线程全部执行完毕后,才可以输出最终结果,其在多线程开发场景下,非常的常用。
结语
本篇我们介绍了CyclicBarrier的用法与实现机制,读完后大家可能发现,CyclicBarrier的实现并不复杂,其核心主要依赖于ReentrantLock与Condition,如果您对这两个类的机制不是很了解的话,建议去看看前面的文章对于它们的介绍。
聊聊并发:(九)concurrent包之ReentrantLock分析
聊聊并发:(十一)concurrent包之Condition源码分析
感谢您的阅读!!!
下篇预告:
聊聊并发:(十三)concurrent包并发辅助类之CountDownLatch源码分析
敬请期待~
更多Java干货文章请关注我的个人微信公众号:老宣与你聊Java