大家好,牧码心今天给大家推荐一篇并发编程系列(十二)—深入理解CyclicBarrier栅栏的文章,希望对你有所帮助。内容如下:
CyclicBarrier也是一个同步辅助器,功能和CountDownLatch有些类似。CountDownLatch是一个倒数计数器,在计数器不为0时,所有调用await的线程都会等待,当计数器降为0,线程才会继续执行,且计数器一旦变为0,就不能再重置了。
CyclicBarrier可以认为是一个可循环栅栏屏障,让一组线程达到栅栏屏障(也叫同步点)时被阻塞。直到最后一个线程达到屏障时。屏障才会打开,所有被屏障阻塞的线程才会继续运行。为了更好的理解,可以看下图所示:
如一共4个线程A、B、C、D,它们到达栅栏的顺序可能各不相同。当A、B、C到达栅栏后,由于没有满足总数【4】的要求,所以会一直等待,当线程D到达后,栅栏才会放行。
对比CountDownLatch有以下不同点:
1.CountDownlatch是允许1或N个线程等待其他线程完成执行。CyclicBarrier 是允许N个线程相互等待;
2. CountDownlatch的计数器无法被重置。CyclicBarrier 的可以被重置后循环使用,也叫循环的CyclicBarrier ;
模拟场景
5个运动员准备跑步比赛,运动员在赛跑前会准备一段时间,当裁判发现所有运动员准备完毕后,就举起发令枪,比赛开始。
这里的起跑线就是屏障,运动员必须在起跑线等待其他运动员准备完毕
public class CyclicBarrierTest {
// 定义同时到达barrier的线程个数
private static final int BARRIER_SIZE=5;
// 定义barrier
private static CyclicBarrier barrier;
public static void main(String[] args) {
barrier=new CyclicBarrier(BARRIER_SIZE, new Runnable() {
@Override
public void run() {
System.out.println("=========所有运动员准备完毕,开始起跑!========");
}
});
for(int i=0;i<5;i++){
// 创建子线程
new SubBarrierThread("运动员"+i).start();
}
}
static class SubBarrierThread extends Thread{
public SubBarrierThread(String name){
super(name);
}
@Override
public void run() {
try {
// 模拟运动员准备
Thread.sleep(500);
System.out.println(Thread.currentThread().getName() + " 已准备完毕!....");
// 远动员准备好,barrier的数量+1
barrier.await();
// 达到barrier的数量为5时,才继续
//System.out.println(Thread.currentThread().getName() + " continued.");
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
说明:从输出可以看到,线程到达栅栏时会被阻塞(调用await方法),直到到达栅栏的线程数满足指定数量要求时,栅栏才会打开放行。
细心的朋友会发现,若线程在阻塞过程中可能被中断,而CyclicBarrier放行的条件是等待的线程数达到指定数目,万一线程被中断导致最终的等待线程数达不到栅栏的要求怎么办?CyclicBarrier一定有考虑到这种异常情况,不然其它所有等待线程都会无限制地等待下去。
那么CyclicBarrier是如何处理的呢?我们看下CyclicBarrier的await()方法定义:
public int await() throws InterruptedException, BrokenBarrierException {
// 业务逻辑
}
可以看到await() 方法除了抛出InterruptedException异常外,还会抛出BrokenBarrierException。
BrokenBarrierException异常表示当前的CyclicBarrier已经损坏了,可能等不到所有线程都到达栅栏了,所以已经在等待的线程也没必要再等了,可以散伙了。
出现以下几种情况之一时,当前等待线程会抛出BrokenBarrierException异常:
1.其它某个正在await等待的线程被中断了
2.其它某个正在await等待的线程超时了
3.某个线程重置了CyclicBarrier(调用了reset方法,后面会讲到)
另外,只要正在Barrier上等待的任一线程抛出了异常,那么Barrier就会认为肯定是凑不齐所有线程了,就会将栅栏置为损坏(Broken)状态,并传播BrokenBarrierException给其它所有正在等待(await)的线程。
CyclicBarrier 依赖独占锁实现,我们看下其UML类图和主要函数。
// 初始化一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,并在启动 barrier 时执行给定的屏障操作
CyclicBarrier(int parties, Runnable barrierAction)
// 在所有参与者都已经在此 barrier 上调用 await 方法之前,将一直等待。
int await()
// 在所有参与者都已经在此屏障上调用 await 方法之前将一直等待,或者超出了指定的等待时间。
int await(long timeout, TimeUnit unit)
// 返回要求启动此 barrier 的参与者数目。
int getParties()
// 查询此屏障是否处于损坏状态。
boolean isBroken()
// 重置屏障重置为其初始状态。
void reset()
CyclicBarrier 通过ReentrantLock(独占锁)和Condition来实现的。下面,我们分析CyclicBarrier中3个核心函数: 构造函数, await()作出分析
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
// parties 表示必须同时到达栅栏点的线程数
this.parties = parties;
// count表示“处在等待状态的线程个数”。
this.count = parties;
// barrierCommand 表示栅栏点的线程个数都达到时,触发的动作;
this.barrierCommand = barrierAction;
}
public class CyclicBarrier {
private final ReentrantLock lock = new ReentrantLock();
private final Condition trip = lock.newCondition();
// 栅栏开启需要的到达线程总数
private final int parties;
// 最后一个线程到达后执行的任务
private final Runnable barrierCommand;
// 剩余未到达的线程总数
private int count;
// 当前轮次的运行状态
private Generation generation = new Generation();
// ...
}
我们可以看到其内部依赖了ReentrantLock 和Condition。这里需要注意的是generation变量,它一个静态内部类定义:
// 定义内部类
private static class Generation {
// broken标识栅栏是否损坏,false标识没有
boolean broken = false;
}
// 唤醒等待的下一组线程
private void nextGeneration() {
// signal completion of last generation
trip.signalAll();
// set up next generation
count = parties;
generation = new Generation();
}
说明:CyclicBarrier 是可以循环复用的,所以CyclicBarrier 的每一轮任务都需要对应一个generation 对象。generation 对象内部有个broken字段,用来标识当前轮次的CyclicBarrier 是否已经损坏。nextGeneration方法用来创建一个新的generation 对象,并唤醒所有等待线程,重置内部参数。
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
阻塞操作await()实际调用的是内部的dowait()方法,具体实现如下:
private int dowait(boolean timed, long nanos)throws InterruptedException, BrokenBarrierException, TimeoutException {
final ReentrantLock lock = this.lock;
// 获取独占锁(lock)
lock.lock();
try {
// 保持当前的generation
final Generation g = generation;
// 若当前栅栏损坏,则抛出异常
if (g.broken)
throw new BrokenBarrierException();
// 如果当前线程被中断,则通过breakBarrier()终止CyclicBarrier,唤醒CyclicBarrier中所有等待线程。
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
// 将处在等待状态的线程个数count减一
int index = --count;
if (index == 0) { //如果index=0,则意味着当前线程为最后一个等待线程。
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
command.run(); // 执行唤醒后的任务
ranAction = true;
nextGeneration(); // 唤醒所有等待的线程,并开始下一轮
return 0;
} finally {
if (!ranAction) // 若任务执行失败,唤醒所有等待的线程
breakBarrier();
}
}
// 自旋,使当前线程一直阻塞,直到满足栅栏条件或 当前线程被中断”或 超时这3者之一发生才继续执行
for (;;) {
try {
if (!timed) // // 如果不是超时等待,则调用awati()进行等待;否则,调用awaitNanos()进行限时等待。
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();
if (g != generation)
return index;
if (timed && nanos <= 0L) { // 若等待超时,则通过breakBarrier()终止CyclicBarrier,唤醒CyclicBarrier中所有等待线程。
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
说明:从dowait() 方法的实现逻辑,可以从以下几点看出CyclicBarrier 如何实现阻塞和唤醒:
1.dowait() 方法作用是阻塞当前线程,直到满足栅栏条件(没有等待的线程了)或当前线程被中断,或等待超时,当前线程才会继续执行;
2.在CyclicBarrier中,同一批的线程属于同一代,即同一个Generation;CyclicBarrier中通过generation对象来记录属于哪一代。当所有线程唤醒后会重置栅栏开启需要的到达线程总数;
3.若当前线程被中断,即Thread.interrupted()为true时,会通过breakBarrier()终止CyclicBarrier。同时唤醒所有的等待线程;
4.通过计数器count做自减操作,若满足栅栏开启条件,则会执行后续动作,同时唤醒所有等待线程,初始化计数器;
5.采用自旋操作阻塞当前线程。