前言
CountDownLatch 可以实现多个线程协调,在所有制定的线程到达屏障(栅栏)后,主线程才执行任务。
但是,CountDownLatch 有个缺陷,它只能使用 1 次。而在有些场合,似乎有点浪费,因为需要不停创建 CountDownLatch 实例。JDK 在 CountDownLatch 的文档中向我们介绍了 CyclicBarrier,方便我们重复完成某个任务。
源码分析
内部成员
/** 重入锁,栅栏的保护者 */
private final ReentrantLock lock = new ReentrantLock();
/** 进入Condition Queue,等待同一个 Generation 的其它线程 */
private final Condition trip = lock.newCondition();
/** 参与者(线程)数量,构造时指定。用于控制什么时候释放打开栅栏,让所有线程通过 */
private final int parties;
/** 所有线程执行完毕后,另外执行的任务,构造时指定 */
private final Runnable barrierCommand;
/** 当代的标识 */
private Generation generation = new Generation();
/**
正在等待着的参与者数量,每一代都会从参与者数量倒数至 0。
在栅栏被破坏或开启新一代时会重置参与者数量
*/
private int count;
在 CyclicBarrier 中,有个“代”的概念。因为 CyclicBarrier 是可以复用的,那么每次所有的线程通过了栅栏,就表示一代过去了,就如同跨年。
构造方法
CyclicBarrier 支持在所有线程通过栅栏的时候,另外执行一个任务。
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;
}
await 方法
CyclicBarrier 最重要的方法是 await,当执行了这个方法,就是树立了一个栅栏,将所有的线程挡住了。只有所有的线程都到了这个栅栏的时候,栅栏才会打开。
举个例子,报团出去旅游时,导游经常会把我们拉到金银首饰等消费品店,通知我们自由活动,等 1 个小时后集合。经过 1 个小时,导游必须等到所有的游客都到齐后才能通知司机开车,然后前往下个消费品店。
await 方法调用的 dowait 方法。
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()) {
// 将损坏状态设置为 true
// 并通知其他阻塞在此栅栏上的线程
breakBarrier();
throw new InterruptedException();
}
// 获取下标
int index = --count;
// 如果是 0 ,说明到头了
if (index == 0) { // tripped
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
// 执行栅栏任务
if (command != null)
command.run();
ranAction = true;
// 更新一代,将 count 重置,将 generation 重置.
// 唤醒之前等待的线程
nextGeneration();
// 结束
return 0;
} finally {
// 如果执行栅栏任务的时候失败了,就将栅栏失效
if (!ranAction)
breakBarrier();
}
}
for (;;) {
try {
// 如果没有时间限制,则直接等待,直到被唤醒
if (!timed)
trip.await();
// 如果有时间限制,则等待指定时间
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
// g == generation >> 当前代
// ! g.broken >>> 没有损坏
if (g == generation && ! g.broken) {
// 让栅栏失效
breakBarrier();
throw ie;
} else {
// 上面条件不满足,说明这个线程不是这代的.
// 就不会影响当前这代栅栏执行逻辑.所以,就打个标记就好了
Thread.currentThread().interrupt();
}
}
// 当有任何一个线程中断了,会调用 breakBarrier 方法.
// 就会唤醒其他的线程,其他线程醒来后,也要抛出异常
if (g.broken)
throw new BrokenBarrierException();
// g != generation >>> 正常换代了
// 一切正常,返回当前线程所在栅栏的下标
if (g != generation)
return index;
// 如果有时间限制,且时间小于等于0,销毁栅栏,并抛出异常
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
nextGeneration 方法
/** 生成下一个 generation */
private void nextGeneration(){
// signal completion of last generation
// 唤醒所有等待的线程来获取 AQS 的state的值
trip.signalAll();
// set up next generation
// 重新赋值计算器
count = parties;
// 重新初始化 generation
generation = new Generation();
}
代码虽然长,但是主体还是挺清晰,这里简单总结一下方法的流程:
1 .每个 CyclicBarrier 都有一个 Lock,想执行 await 方法,就必须获得这把锁。所以,CyclicBarrier 在并发情况下的性能是不高的。
- 一些线程中断的判断,注意,CyclicBarrier 中,只要有一个线程中断了,其余的线程也会抛出中断异常。并且,这个 CyclicBarrier 就不能再次使用了。
- 每次线程调用一次 await 方法,表示这个线程到了栅栏这里了,那么就将计数器减一。如果计数器到 0 了,表示这是这一代最后一个线程到达栅栏,就尝试执行我们构造方法中输入的任务。最后,将代更新,计数器重置,并唤醒所有之前等待在栅栏上的线程。
- 如果不是最后一个线程到达栅栏了,就使用 Condition 的 await 方法阻塞线程。如果等待过程中,线程中断了,就抛出异常。这里,注意一下,如果中断的线程的使用 CyclicBarrier 不是这代的,比如,在最后一次线程执行 signalAll 后,并且更新了这个“代”对象。在这个区间,这个线程被中断了,那么,JDK 认为任务已经完成了,就不必在乎中断了,只需要打个标记。所以,catch 里的 else 判断用于极少情况下出现的判断——任务完成,“代” 更新了,突然出现了中断。这个时候,CyclicBarrier 是不在乎的。因为任务已经完成了。
- 当有一个线程中断了,也会唤醒其他线程,那么就需要判断 broken 状态。
g != generation
所有线程到达 barrier 直接返回
总结
从 await 方法看,CyclicBarrier 还是比较简单的,JDK 的思路就是:设置一个计数器,线程每调用一次计数器,就减 1,并使用 Condition 阻塞线程。当计数器是 0 的时候,就唤醒所有线程,并尝试执行构造函数中的任务。由于 CyclicBarrier 是可重复执行的,所以,就需要重置计数器。