在前一章节我们详细分析了Semaphore
以及CountDownLatch
的基本用法及实现原理,并且在将CountDownLatch
时,也举了一个运动员赛跑的栗子。下面我将接着这个栗子详细分析CyclicBarrier
的基本用法及底层实现原理。
CyclicBarrier
- 顾名思义,是一个可被循环使用的屏障。在前一章讲述CountDownLatch的时候说道,
CountDownLatch
可以用在等待指定的线程数执行完成,即到达一个指定的时间节点后,同时开始其他的任务。而CyclicBarrier
在其上做了更深层次的优化—当达到一个指定的节点后,CyclicBarrier会被重置,从而可以再次被利用,等待线程到达下一个时间节点。
还是沿用上一章节中,运动员的栗子。运动员需要准备,裁判需要等待所有运动员准备好后,才能开始鸣枪开始,这里是第一个时间节点,跑完后,需要等所有运动员都到达终点后,才能开始统计各个运动员的名次,这是第二个时间节点,需要等待所有运动员的名次统计好后,才能举行颁奖,这又是第三个时间节点。这个时候,如果使用
CountDownLatch
实现的,我们需要同时定义三个CountDownLatch对象,因为CountDownLatch中的state变量为0后,不会被重置为初始值,所以不能被重复循环利用。此时可以用CyclicBarrier
来解决这个问题。
/*
*runner-2 is Ready!
runner-3 is Ready!
runner-1 is Ready!
runner-4 is Ready!
runner-5 is Ready!
=================All runners ready============
runner-2 arrive!
runner-4 arrive!
runner-1 arrive!
runner-3 arrive!
runner-5 arrive!
=================All runners ready============
runner-2 has been recorded...
runner-1 has been recorded...
runner-4 has been recorded...
runner-5 has been recorded...
runner-3 has been recorded...
=================All runners ready============
**/
ublic class CyclicBarrierDemo {
public static void main(String[] args) {
String prefix = "runner-";
// 指定需要等待5个运动员同时完成某件事后,再执行某项任务
// 同时指定了一个lambda表达式作为第二个参数,这个参数表示所有线程到达后,执行的任务(有最后一个达到的线程执行)
CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> System.out.println("=================All runners ready============"));
// 模拟五个运动员,即五个线程
IntStream.rangeClosed(1, 5).forEach(i -> new Runner(cyclicBarrier, prefix + i).start());
}
private static class Runner extends Thread {
private String name;
private CyclicBarrier cyclicBarrier;
public Runner(CyclicBarrier cyclicBarrier, String name) {
this.cyclicBarrier = cyclicBarrier;
this.name = name;
}
@Override
public void run() {
try {
// 随机休眠一段时间:模拟准备时间
sleepRandom();
System.out.println(this.name + " is Ready!");
// 同步阻塞,等待其他运动员准备完成
cyclicBarrier.await();
// 开始跑步后,模拟各个运动员跑步时间
sleepRandom();
System.out.println(this.name + " arrive!");
// 等待所有运动员到达现场
cyclicBarrier.await();
// 模拟记录运动员名次的事件
sleepRandom();
System.out.println(this.name + " has been recorded...");
// 等待所有运动员,被记录,开始颁奖
cyclicBarrier.await();
} catch (Exception e) {
System.out.println(this.name + " Error");
}
}
}
}
- 源码分析:
CyclicBarrier
没有直接基于AQS实现同步过程,而是通过ReentrantLock
+Condition
的方式实现的。
首先一起来看一下其构造函数:构造函数比较简单,就是完成基本的赋值操作
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
// 指定需要等待的线程总数
this.parties = parties;
// 记录仍然需要等待的线程个数:每达到一个线程处于阻塞状态时,count-1,
// 当count为0时,唤醒所有等待的线程
this.count = parties;
// 指定当parties个线程达到,即count为0时,执行的回调方法。
this.barrierCommand = barrierAction;
}
接下来重点分析
await()
方法,这个方法是CyclicBarrier
的核心方法。
// 方法声明抛出InterruptedException,表示可以相应中断
// BrokenBarrierException表示可以中断一次屏障,即打破CyclicBarrier中线程阻塞的状态,使其唤醒
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
// 获取当前锁
lock.lock();
try {
// 定义了一个Generation内部类,表示一次循环
final Generation g = generation;
// 循环被打破,表示该CyclicBarrier不可用,抛出BrakenBarrierException异常
if (g.broken)
throw new BrokenBarrierException();
// 响应线程中断
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
// 计数器减一,即表示一个线程任务到达。
int index = --count;
// 减为0后,即count==0,此时index==count,表示当前循环所有任务都到达,需唤醒阻塞的线程
if (index == 0) { // tripped
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
// 这里就是构造函数中设置的回调。不为null,则会执行回调,且只会由最后一个到达的线程执行。
if (command != null)
command.run();
// ranAction设置为true,表示需要唤醒阻塞的线程,继续往下执行,而不是阻塞,false表示需要打破这次循环
ranAction = true;
// 开启下一代,即下一轮循环:其实就是设置count重新等于parties
nextGeneration();
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
}
// loop until tripped, broken, interrupted, or timed out
for (;;) {
try {
// 判断当前调用的是超时等待await(timeout,unit),还是一直等待await()
if (!timed)
// 阻塞当前线程,同时释放锁:此处的trip就是该Lock的Condition。对Condition不了解的同学可以看前面章节。
trip.await();
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
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)
throw new BrokenBarrierException();
if (g != generation)
return index;
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
// 开始下一次循环:主要完成三件事:
// 1. trip.signalAll();唤醒所有阻塞在trip上的线程
// 2. count = parties;将需要等待的线程数重新设置回原来的parties值,以便开始下一次循环
// 3. generation = new Generation();开始下一次循环,broken默认为false
private void nextGeneration() {
// signal completion of last generation
trip.signalAll();
// set up next generation
count = parties;
generation = new Generation();
}
// 打破循环,也主要完成三件事
// 1. generation.broken = true;将当前循环的broken设置为true,后续加入CyclicBarrier的线程,即执行await()方法时,会抛出异常
// 2. count = parties;恢复count的值,同上
// 3. trip.signalAll();唤醒所有阻塞的线程。
private void breakBarrier() {
generation.broken = true;
count = parties;
trip.signalAll();
}
通过上述分析可知:
- 无论是调用
nextGeneration()
还是调用breakBarrier()
都会唤醒当前阻塞的所有线程,只是前者会开启下一代,即开始下一轮循环等待。而后者不会开启下一代,且禁止线程再次加入CyclicBarrier
的阻塞队列中来。CyclicBarrier
是可重用的。等待线程到达后,会开启下一代,每一轮循环,被称为一个Generation,也就是一个同步点,一代。CyclicBarrier
会响应中断。当线程没有到齐时,如果有线程接收到了中断信号,所有阻塞的线程都会被唤醒,且其他线程无法再加入CyclicBarrier
可以指定回调函数,且回调函数只会被最后到达的线程执行,而不是每个线程都执行。