当你有这种需求:想要每执行N个线程后就执行某个任务,那么你就可以用CyclicBarrier。
举个例子:
四个线程,每个线程在执行完自己的任务后停下来等待其它三个线程执行完任务,全部执行完后,四个线程继续往下执行。
public class CyclicBarrierTest {
static class Writer extends Thread {
private CyclicBarrier barrier;
public Writer(CyclicBarrier barrier) {
this.barrier = barrier;
}
@Override
public void run() {
System.out.println("Thread name " + Thread.currentThread().getName() + " is writing");
try {
Thread.sleep(5000);
System.out.println("Thread name " + Thread.currentThread().getName() + " write over");
barrier.await();
} catch (TimeoutException e) {
barrier.reset();
} catch (BrokenBarrierException e){
System.out.println(Thread.currentThread().getName() + " 因拦截损坏被抛弃");
} catch (InterruptedException e){
barrier.reset();
}
System.out.println(Thread.currentThread().getName() + " can start forward");
}
}
public static void main(String[] args) {
final int n = 4;
//CyclicBarrier支持一个可选的Runnable命令,在一组线程中的最后一个线程到达之后
//(但在释放所有线程之前),该命令只在每个屏障点运行一次
final CyclicBarrier barrier = new CyclicBarrier(n,
() -> System.out.println(Thread.currentThread().getName() +" thread to do this runnable"));
for (int i = 0; i < n; i++) {
new Writer(barrier).start();
}
}
}
代码中我们对TimeoutException以及InterruptedException 处理是调用reset方法,否则一旦发生这两种异常,之后所有线程都将抛BrokenBarrierException 异常。
运行结果:
Thread name Thread-0 is writing
Thread name Thread-2 is writing
Thread name Thread-1 is writing
Thread name Thread-3 is writing
Thread name Thread-3 write over
Thread name Thread-0 write over
Thread name Thread-1 write over
Thread name Thread-2 write over
Thread-2 thread to do this runnable
Thread-2 can start forward
Thread-3 can start forward
Thread-1 can start forward
Thread-0 can start forward
源码
先来看看构造函数以及成员变量
private final ReentrantLock lock = new ReentrantLock(); //重入锁
private final Condition trip = lock.newCondition(); // 等待队列
private final int parties; //每次要拦截的线程数
private final Runnable barrierCommand; //你想在每N个线程后执行的任务
//parties-当前阻塞的线程数
private int count;
//每N个线程为一次拦截循环,该值标识本次拦截是否损坏,中断与超时异常将会损坏本次拦截
private Generation generation = new Generation();
private static class Generation {
boolean broken = false; //标识当前的barrier是否已“损坏”
}
public CyclicBarrier(int parties) {
this(parties, null);
}
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
Generation是CyclicBarrier的一个私有内部类,他只有一个成员变量来标识当前的barrier是否已“损坏”:如果任何线程在等待时被中断(等待中的中断分两种,分别对应两种处理REINTERRUPT与THROW_IN,只有后者才是这里所指的),则之后所有线程都将抛出 BrokenBarrierException 异常。
对于失败的同步尝试,CyclicBarrier 使用了一种要么全部要么全不 (all-or-none) 的破坏模式。
await
对于返回值:例如上面例子中parties=4,若一个线程await得到返回值3,则说明它是第一个到达栅栏的线程,也可以说还有三个未到,返回值为0则说明该线程为最后一个到的,全部线程都已到达。
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
dowait
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock(); //锁
try {
final Generation g = generation; // 获取generation
if (g.broken) // 若generation损坏,抛BrokenBarrierException
throw new BrokenBarrierException();
if (Thread.interrupted()) { // 线程中断
// 标记generation为损坏,重置count为parties,唤醒Condition等待队列中的所有线程
breakBarrier();
throw new InterruptedException(); // 抛InterruptedException
}
int index = --count; // 线程到此说明已到栅栏前,count需减1
if (index == 0) { // 说明parties个线程都已到栅栏前
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
command.run(); // 运行barrierCommand
ranAction = true;
//唤醒Condition所有等待线程,重置count为parties,更新generation
nextGeneration();
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
}
//运行到这说明还有index个线程未到,要做的便是阻塞当前线程
// loop until tripped, broken, interrupted, or timed out
for (;;) {
try {
if (!timed) // 是否有时限
trip.await(); // 会构建当前线程的节点放入等待队列,释放锁,阻塞线程
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
//若中断异常,判断当前线程的generation是否毁损,没有则breakBarrier并抛异常,否则中断当前线程
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();
}
}
if (g.broken) // 损坏抛异常
throw new BrokenBarrierException();
if (g != generation)//
return index;
if (timed && nanos <= 0L) { // 超时,breakBarrier并抛TimeoutException
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
可以看到代码中多处会进行generation,generation.broken以及中断状态的判断并进行相关处理,那么它们究竟解决了那些情况,对这进行探究会帮助更好理解CyclicBarrier。
以await()调用为例,初始设置有四个线程A,B,C,D,A线程执行完自己的业务逻辑,调用await——>dowait,执行到lock(这里的lock指的是ReentrentLock),BCD执行完业务由于lock阻塞就得等待被放入到同步队列(指由AQS维护的队列,有别于ConditionObject的等待队列)中等待,接着A会进入for循环执行trip.await(),这里调用了ConditionObject的await方法,之前文章介绍过它,此时A就会被放入到等待队列中等待,过程中release释放锁,唤醒在同步队列的后继节点线程。(假设同步队列中顺序是B->C->D)B就会被唤醒,执行A一样的步骤,最后被放入等待队列;C也一样;最后轮到D,index == 0 为true,barrierCommand被执行,之后调用nextGeneration
private void nextGeneration() {
// 调用了ConditionObject的signalAll,之前分析过,将所有条件队列中的节点放回到同步队列,
trip.signalAll();
// CyclicBarrier是可重复的,实现就在这,重置count与generation
count = parties;
generation = new Generation();
}
之后D返回之前执行lock.unlock(),释放锁唤醒同步队列等待的ABC。
这里选择的是ReentrantLock的非公平锁,它对上述过程的影响?以及并发过程中同步队列与等待队列的变化?CyclicBarrier功能如何实现的?
首先在parties个线程到达之前,CondiyionObject的等待队列中有partie-count个已到达栅栏的线程节点,它们的代码停在ConditionObject的await方法里,同步队列中是运行CyclicBarrier的await争夺独占锁失败的线程。当最后一个到达栅栏的线程执行,如上面的D,等待队列的节点线程被全部放回同步队列,此时同步队列中就有两种节点。
由于D调用了nextGeneration,count与generation被重置,新的栅栏生成它会将拦住parties个接下来抢到锁运行的线程,不过当由等待队列放回同步队列的节点线程在同步队列中被唤醒后,若成功拿到锁,并不会被这个新的栅栏拦住,为什么?
从代码来看,这些线程从ConditionObject的await里开始继续执行(该过程在ReentrantLock分析过),之后回到CyclicBarrier的await从trip.await()那一行开始往下,所以便不会被拦住。
若是在被拦截等待过程中发生中断,超时,generation损坏,generation更新等问题,CyclicBarrier是如何处理的?
先来看看CyclicBarrier的await里下面的这段
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();
}
}
线程运行到这说明不是最后一个线程,需要等待,调用ConditionObject的await,该方法首先会检查中断标记,分两种情况考虑:1,若是已经发生了中断则抛InterruptedException,节点没放入等待队列 ,代码回到这里异常被捕获,这种情况下generation没有被更新,若是也没有损坏的话,调用breakBarrier
private void breakBarrier() {
generation.broken = true;
count = parties;
trip.signalAll();
}
该方法调用后会发生什么?所有之后运行的线程都将抛BrokenBarrierException异常,因为generation.broken等于true,所以对于调用CyclicBarrier的await方法线程应该处理InterruptedException 异常,调用reset方法处理该异常。
public void reset() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
breakBarrier(); // break the current generation
nextGeneration(); // start a new generation
} finally {
lock.unlock();
}
}
该方法等于将CyclicBarrier回归初始状态,等待队列全部被放回同步队列,count重置为parties,generation更新。
2,第一种情况是在节点调用ConditionObject的await之前发生中断,若是之后发生则线程会构成节点放入等待队列,无论是之后什么时候发生,ConditionObject的await只会记录该异常,根据情况选择处理方式REINTERRUPT或THROW_IN,线程仍会阻塞在等待队列中,直到被最后一个到栅栏的线程唤醒,节点回到同步队列,generation已经更新,等到这些节点线程被唤醒执行,代码回到ConditionObject的await方法,若是THROW_IN,抛异常,回到CyclicBarrier的await被catch,由于generation已经更新,处理方式是恢复该异常Thread.currentThread().interrupt(),让更上层也就是到我们的业务代码去处理,这种情况不像第一种意味着栅栏的损坏,该种情况并不会影响栅栏。
tryCatch语句后有三个if判断,它们起到什么作用?
if (g.broken) // 损坏抛异常
throw new BrokenBarrierException();
if (g != generation)//
return index;
if (timed && nanos <= 0L) { // 超时,breakBarrier并抛TimeoutException
breakBarrier();
throw new TimeoutException();
}
一个栅栏要拦截N个线程,已有k个线程被拦截等待在等待队列中,若是有一个线程发生情况1,而该线程对其处理是reset,那么对于这k个线程,此时它们已经在同步队列中等待,等到被唤醒后,假设它们没发生异常,离开tryCatch来到第一个 if 判断,此时g.broken为true,所以这K个都会抛BrokenBarrierException异常。再次重申CyclicBarrier的功能:每N个线程执行完就执行一个特定任务。若一次拦截中有线程发生情况一,则抛弃本次已拦截k个线程,重新开始拦截。
对于第二个 if 判断,一次generation成功,除了最后一个被拦截线程外其余线程都将在这个 if 里返回,返回值代表剩余需要拦截的线程数。
对于第三个 if 判断,是超时处理,nanos <= 0L代表等待超时,调用breakBarrier抛弃本次generation已经拦截的线程,抛TimeoutException,用户代码catch到该异常应该reset,否则之后所有线程都将抛BrokenBarrierException异常。