前言
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) { // trippedboolean 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;// 重新初始化 generationgeneration = 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 是可重复执行的,所以,就需要重置计数器。
