##应用场景与 countDownLatch差不多。
一种同步辅助工具,允许一组线程都等待彼此到达一个共同的障碍点。cyclicbarrier在涉及固定大小的线程组的程序中很有用,这些线程偶尔必须等待对方。这个屏障被称为循环的,因为它可以在等待的线程被释放后重新使用。
例如一个团队游戏,总共10人参加,其中有一个项目是推到高墙,必须10个人一起参加,缺一不可。那么先到达墙下的人必须等待,等人齐后一起推。这10个人就相当于10个线程。
CyclicBarrier支持一个可选的Runnable命令,该命令在参与方中的最后一个线程到达之后,但在释放任何线程之前,在每个屏障点运行一次。此屏障操作有助于在任何一方继续之前更新共享状态。
CyclicBarrier字面意思是“可重复使用的栅栏”,CyclicBarrier 相比 CountDownLatch 来说,要简单很多,其源码没有什么高深的地方,它是 ReentrantLock 和 Condition 的组合使用。
比较
countDownLatch:
1、主任务要等待所有的子任务完成后,在执行主任务
2、计数器归零后,就不会在变了。只能设置一重屏障。
3、是基于 AQS 的共享模式的使用。
4、计数器则由使用者来控制,线程调用await方法只是将自己阻塞而不会减少计数器的值。
cyclicBarrier:
1、假设有5个线程,其中4个线程都到达了某个点(屏障处),他们会陷入等待,直到第5个线程也到达了这个点,所有的线程才会继续往下执行。主线程不会被阻塞。
2、数量是可以重用。线程冲破屏障后,会恢复到原来的初始值(构造方法传入的参数)。可以实现多重屏障。
3、是基于 Condition 来实现的。
4、计数器由自己(代码逻辑)控制,线程调用await方法不仅会将自己阻塞还会将计数器减1。
至此我们难免会将CyclicBarrier与CountDownLatch进行一番比较。这两个类都可以实现一组线程在到达某个条件之前进行等待,它们内部都有一个计数器,当计数器的值不断的减为0的时候所有阻塞的线程将会被唤醒。
方法
private static class Generation
{ boolean broken = false; }
- 静态内部类Generation
- 起到多重屏障的作用。Barrier被冲破或重置的时候,generation会发生变化。
- Generation描述着CyclicBarrier的更新换代。在CyclicBarrier中,同一批线程属于同一代。当有parties个线程到达barrier之后,generation就会被更新换代。其中broken标识该当前CyclicBarrier是否已经处于中断状态。
public CyclicBarrier(int parties, Runnable barrierAction)
{ … }
- 创建一个新的CyclicBarrier,它将在给定数量的参与方(线程)等待它触发
- 并在屏障被触发时执行给定的屏障操作,由最后一个进入屏障的线程执行屏障。
- ( barrierAction 支持一个可选的Runnable命令,该命令在参与方中的最后一个线程到达之后,但在释放任何线程之前,在每个屏障点运行一次。此屏障操作有助于在任何一方继续之前更新共享状态。)
public CyclicBarrier(int parties)
{ this(parties, null); }
- parties 参数方,设置冲破屏障的线程个数
- 并且在屏障被触发时不执行预定义的操作。
public int getParties()
{ … }
- 返回跳过此障碍所需的参与方数。
public int await()
throws InterruptedException, BrokenBarrierException {…}
- 等待直到所有的 parties(参与方),都已经在屏障之前调用了await()方法。
- 如果当前线程不是最后到达的线程,则它将处于休眠状态,直到发生以下情况之一:
- 1、直到所有的 parties(参与方),都已经在屏障之前调用了await()方法;
- 2、其他线程中断当前线程;
- 3、其他线程中断其他等待线程之一;
- 4、其他线程在等待屏障时超时;
- 5、其他线程对此屏障调用reset
public int await(long timeout, TimeUnit unit)
throws InterruptedException, BrokenBarrierException, TimeoutException {…}
- 等待,直到所有参与方都对此屏障调用了wait,或者指定的等待时间已过。
- 如果当前线程不是最后到达的线程,则它将处于休眠状态,直到发生以下情况之一:
- 1、直到所有的 parties(参与方),都已经在屏障之前调用了await()方法;
- 2、其他线程中断当前线程;
- 3、其他线程中断其他等待线程之一;
- 4、其他线程在等待屏障时超时;
- 5、其他线程对此屏障调用reset
- 6、指定的超时将被延迟;
public boolean isBroken()
{ … }
- 查询此屏障是否处于断开状态。
- return generation.broken
public void reset()
{ … }
- 将屏障重置为其初始状态。如果有任何一方目前正在关卡等待,他们将返回BrokenBarrier。
- 请注意,由于其他原因发生中断后的重置可能会很复杂;线程需要以其他方式重新同步,然后选择一种方式来执行重置。更可取的做法是创建一个新的屏障以供后续使用。
public int getNumberWaiting()
{ … }
- 返回当前在屏障处等待的参与方数(parties - count)。此方法主要用于调试和断言。
成员变量
private final ReentrantLock lock
= new ReentrantLock();
- The lock for guarding barrier entry
- 起到多重屏障的作用。Barrier被冲破或重置的时候,generation会发生变化。
private final Condition trip
= lock.newCondition();
- Condition to wait on until tripped
- 屏障状态(线程拦截器)
private final int parties
;
- The number of parties
- 每次屏障前需要拦截的线程总数
private final Runnable barrierCommand
;
- The command to run when tripped
- 屏障被冲破时,需要运行的命令
private Generation generation
= new Generation();
- The current generation
- 表示栅栏的当前代(当前generation)
private int count
;
- 仍在等待的参与方数量,起到计数器的作用。
- 每当有一个线程到达屏障前,count都会减1。直到减到0,线程冲破屏障。
- 之后开始下一个分代,会重新将parties的值赋给count。
案例
一层屏障
案例描述:开启3个线程,每个线程完成的时间不同。所有的线程都完成后,屏障打开。
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
Thread.sleep((long)(Math.random() * 2000));
int randomInt = new Random().nextInt(500);
System.out.println("hello" + randomInt);
cyclicBarrier.await();
System.out.println("world" + randomInt);
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
// 输出信息
// hello318
// hello464
// hello436
// world436
// world318
// world464
双层屏障
案例描述:循环实现双重屏障
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(1);
for (int j = 0; j < 2; j++) {
for (int i = 0; i < 1; i++) {
new Thread(() -> {
...
}).start();
}
}
}
// 输出信息
// j = 0 hello207
// world207
// j = 1 hello451
// world451
源码分析
底层执行流程
- 初始化cyclicBarrier中的各种成员变量,包括parties、count以及Runnable(可选)。
- 当调用await方法时,底层先检查计数器(count)是否已经归零,是的话,那么就首先执行可选的Runnable,接下开始下一个generation(分代、下一重屏障);
- 在下一个分代中,将会重置count值为parties,并且创建新的Generation实例。
- 同时会调用Condition的signalAll方法,唤醒所有在屏障前等待的线程,让其继续执行。
- 如果计数器没有归零,那么当前的调用线程将会通过Condition的await方法,在屏障前进行等待。
- 以上所有执行流程均在lock锁控制范围内,不会出现并发情况。
在CyclicBarrier所有的成员变量中,可以看到CyclicBarrier内部是通过条件队列trip来对线程进行阻塞的,并且其内部维护了两个int型的变量parties和count,parties表示每次拦截的线程数,该值在构造时进行赋值。count是内部计数器,它的初始值和parties相同,以后随着每次await方法的调用而减1,直到减为0就将所有线程唤醒。CyclicBarrier有一个静态内部类Generation,该类的对象代表栅栏的当前代,就像玩游戏时代表的本局游戏,利用它可以实现循环等待。barrierCommand表示换代前执行的任务,当count减为0时表示本局游戏结束,需要转到下一局。在转到下一局游戏之前会将所有阻塞的线程唤醒,在唤醒所有线程之前你可以通过指定barrierCommand来执行自己的任务。我用一图来描绘下 CyclicBarrier 里面的一些概念:
首先从构造函数出发
【CyclicBarrier.class】
# 构造器1
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
# 构造器2
public CyclicBarrier(int parties) {
this(parties, null);
}
# CyclicBarrier有两个构造器,其中构造器1是它的核心构造器,在这里你可以指定本局游戏的参与者数量(要
# 拦截的线程数: parties)以及本局结束时要执行的任务(barrierAction),还可以看到计数器count的初始值
# 被设置为parties。
再看 await()
【CyclicBarrier.class】
# CyclicBarrier类最主要的功能就是使先到达屏障点的线程阻塞并等待后面的线程,其中它提供了两种等待的
# 方法,分别是定时等待和非定时等待。
# 非定时等待
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe);
}
}
# 定时等待
public int await(long timeout, TimeUnit unit) throws InterruptedException,
BrokenBarrierException, TimeoutException {
return dowait(true, unit.toNanos(timeout));
}
# 可以看到不管是定时等待还是非定时等待,它们都调用了dowait方法,只不过是传入的参数不同而已。
# 下面我们就来看看dowait方法都做了些什么。
# 核心等待方法
# 逻辑处理比较简单,如果该线程不是最后一个调用await方法的线程,则它会一直处于等待状态,
# 除非发生以下情况:
# 1、最后一个线程到达,即index == 0
# 2、某个参与线程等待超时
# 3、某个参与线程被中断
# 4、调用了CyclicBarrier的reset()方法。该方法会将屏障重置为初始状态
private int dowait(boolean timed, long nanos) throws InterruptedException,
BrokenBarrierException, TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock();
try {
final Generation g = generation;
# 检查当前栅栏是否被打翻
if (g.broken) {
throw new BrokenBarrierException();
}
# 检查当前线程是否被中断
if (Thread.interrupted()) {
//如果当前线程被中断会做以下三件事
//1.打翻当前栅栏
//2.唤醒拦截的所有线程
//3.抛出中断异常
breakBarrier();
throw new InterruptedException();
}
# 每次dowait()都将计数器的值减1
int index = --count;
# 计数器的值减为0则需唤醒所有线程并转换到下一代
if (index == 0) {
boolean ranAction = false;
try {
# 唤醒所有线程前先执行指定的任务
final Runnable command = barrierCommand;
if (command != null) {
command.run();
}
ranAction = true;
# 唤醒所有线程并转到下一代
nextGeneration();
return 0;
} finally {
# 确保在任务未成功执行时能将所有线程唤醒
if (!ranAction) {
breakBarrier();
}
}
}
# 如果计数器不为0则执行此循环
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();
}
}
# 如果线程因为打翻栅栏操作而被唤醒则抛出异常
if (g.broken) {
throw new BrokenBarrierException();
}
# 如果线程因为换代操作而被唤醒则返回计数器的值。
# g != generation表示正常换代了,返回当前线程所在栅栏的下标
# 如果 g == generation,说明还没有换代,那为什么会醒了?
# 因为一个线程可以使用多个栅栏,当别的栅栏唤醒了这个线程,就会走到这里,所以需要判断是否是
# 当前代。正是因为这个原因,才需要generation来保证正确。
if (g != generation) {
return index;
}
# 如果线程因为时间到了而被唤醒则打翻栅栏并抛出异常
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
上面贴出的代码中注释都比较详细,我们只挑一些重要的来讲。可以看到在dowait方法中每次都将count减1,减完后立马进行判断看看是否等于0,如果等于0的话就会先去执行之前指定好的任务,执行完之后再调用nextGeneration方法将栅栏转到下一代,在该方法中会将所有线程唤醒,将计数器的值重新设为parties,最后会重新设置栅栏代次,在执行完nextGeneration方法之后就意味着游戏进入下一局。如果计数器此时还不等于0的话就进入for循环,根据参数来决定是调用trip.awaitNanos(nanos)还是trip.await()方法,这两方法对应着定时和非定时等待。如果在等待过程中当前线程被中断就会执行breakBarrier方法,该方法叫做打破栅栏,意味着游戏在中途被掐断,设置generation的broken状态为true并唤醒所有线程。同时这也说明在等待过程中有一个线程被中断整盘游戏就结束,所有之前被阻塞的线程都会被唤醒。线程醒来后会执行下面三个判断,看看是否因为调用breakBarrier方法而被唤醒,如果是则抛出异常;看看是否是正常的换代操作而被唤醒,如果是则返回计数器的值;看看是否因为超时而被唤醒,如果是的话就调用breakBarrier打破栅栏并抛出异常。这里还需要注意的是,如果其中有一个线程因为等待超时而退出,那么整盘游戏也会结束,其他线程都会被唤醒。
breakBarrier
dowait ==> breakBarrier
【CyclicBarrier.class】
# 打翻当前栅栏
private void breakBarrier() {
# 将当前栅栏状态设置为打翻
generation.broken = true;
# 设置计数器的值为需要拦截的线程数
count = parties;
# 唤醒所有线程
trip.signalAll();
}
nextGeneration
dowait ==> nextGeneration
【CyclicBarrier.class】
# 切换栅栏到下一代
private void nextGeneration() {
# 唤醒条件队列所有线程
trip.signalAll();
# 设置计数器的值为需要拦截的线程数
count = parties;
# 重新设置栅栏代次
generation = new Generation();
}
最后,看怎么重置一个栅栏
【CyclicBarrier.class】
public void reset() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
breakBarrier(); // break the current generation
nextGeneration(); // start a new generation
} finally {
lock.unlock();
}
}
我们设想一下,如果初始化时,指定了线程 parties = 4,前面有 3 个线程调用了 await 等待,在第 4 个线程调用 await 之前,我们调用 reset 方法,那么会发生什么?
首先,打破栅栏,那意味着所有等待的线程(3个等待的线程)会唤醒,await 方法会通过抛出 BrokenBarrierException 异常返回。然后开启新的一代,重置了 count 和 generation,相当于一切归零了。
注:源码分析内容主要来源晨初听雨