JUC 之 CyclicBarrier 源码分析
上一篇我们介绍了 CountDownLatch 的实现原理,它使用 state 变量来表明等待线程的数量,而本篇的 CyclicBarrier 被称为线程屏障,即让一组线程(也是通过 AQS 的 state 变量来记录线程个数)都到达某个屏障点,然后所有线程一起继续执行。下面我们先来看个使用示例
static class Solver {
final int N;
final CyclicBarrier barrier;
class Worker implements Runnable {
int myRow;
Worker(int row) {
myRow = row;
}
public void run() {
/*while (!done()) {}*/
processRow(myRow);// 业务处理逻辑
try {
barrier.await();// 业务处理完,等待其它线程处理完,然后同时往下执行
} catch (Exception ex) {}
ThreadUtil.printMsg("work done: " + myRow);
}
}
public Solver(int num) {
N = num;
Runnable barrierAction = () -> ThreadUtil.printMsg("barrierAction");// 所有线程到达屏障点后回调该任务
barrier = new CyclicBarrier(N, barrierAction);// 创建 CyclicBarrier 对象
List<Thread> threads = new ArrayList<>(N);
for (int i = 0; i < N; i++) {
Thread thread = new Thread(new Worker(i));
threads.add(thread);
thread.start();
}
for (Thread thread : threads) {
try {
thread.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
上述例子,我们让每个线程单独处理各自业务,处理完成后调用 CyclicBarrier 的 await 方法等待其它线程处理完各自任务到达同一屏障点,然后统一唤醒所有阻塞线程往下执行各自剩余业务,然后退出。直到怎么使用后下面我们开始源码学习。
核心成员
通过下述信息我们看到 CyclicBarrier 并不像 CountDownLatch 一样直接操作 AQS 的 state 值,而是通过 ReentrantLock 和 Condition 来实现的。这两个类的源码已在【JUC 之 ReentrantLock 源码解析】和 【AQS 之 Condition 源码剖析】两篇中详细讲解过,详情请查看这两篇的介绍。
// 与 CountDownLatch 不一样的是 CyclicBarrier 可以通过 reset 方法来复用,每次开启新的屏障时都会创建一个新的 Generation 对象,用于表示当前轮次
private static class Generation {
boolean broken = false;
}
// 用于保护屏障操作
private final ReentrantLock lock = new ReentrantLock();
// 用于等待所有线程都到达屏障点的条件变量
private final Condition trip = lock.newCondition();
// 表示当前轮次需要到达屏障点的线程数
private final int parties;
// 所有线程到达屏障点后的回调任务
private final Runnable barrierCommand;
// 表示当前轮次
private Generation generation = new Generation();
// 用于表示还有多少线程没有到达屏障点
private int count;
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties; // 初始值 = parties
this.barrierCommand = barrierAction;
}
await 方法
现成到达指定屏障点时调用 该方法阻塞线程等待,具体实现用 dowait 方法实现
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
dowait 方法
1、当 count 值变为 0 时,代表是最后一个到达屏障点的线程,该线程将调用 nextGeneration 方法先唤醒所有等待在条件队列上的线程,然后重置 count 和 Generation(CyclicBarrier可重复使用的原因所在)
2、count != 0时,线程将调用 Condition 的 await 方法阻塞等待,
private int dowait(boolean timed, long nanos)throws InterruptedException, BrokenBarrierException, TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock();// 获取锁,因为操作 Condition 条件变量时需要线程现持有锁
try {
final Generation g = generation;// 保存当前轮次的 Generation
if (g.broken)// 当前轮次出现异常了,有线程中途退出或执行异常了导致本轮次无法正常进行
throw new BrokenBarrierException();
if (Thread.interrupted()) {// 现成本中断了
breakBarrier();// 终止本轮次的操作,并抛出中断异常
throw new InterruptedException();
}
int index = --count;// 执行到这里说明该线程执行正常,对 count 减一 操作(线程安全)
if (index == 0) { // 等于 0 表明最后一个线程达到了屏障点
boolean ranAction = false;
try {
final Runnable command = barrierCommand;// 回调 barrierCommand 任务
if (command != null)
command.run();
ranAction = true;
nextGeneration();// 还原 count,创建一个新的 Generation 对象,并唤醒所有等待在屏障点的线程
return 0;
} finally {
if (!ranAction)// barrierCommand 任务执行异常,
breakBarrier(); // 终止本轮次的进行,将 Generation 的 broken 设置为 true(其它线程将会检测到该值,然后纷纷退出本轮次的操作),当前线程退出本轮次
}
}
// 走到for(;;)循环说明不是最后一个到达屏障点的线程
for (;;) {
try {
if (!timed)// 是否超时等待
trip.await();// 无限期等待,await 原理请查看 【AQS 之 Condition 源码剖析】一篇
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) // 检测到 broken 被设置为了 true,有别的现成退出了该轮次
throw new BrokenBarrierException();
if (g != generation)// Generation 发生了变更,说明已经处于下一轮次当中,本轮次直接退出即可
return index;
if (timed && nanos <= 0L) {// 超时退出
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
private void nextGeneration() {
// 唤醒所有等待线程
trip.signalAll();
// 重置 count 值
count = parties;
generation = new Generation();// 表示进入下一轮次
}
private void breakBarrier() {
generation.broken = true;
count = parties;
trip.signalAll();// 唤醒所有等待线程,各自线程如果检测到 generation.broken 被设置为了 ture ,然后纷纷抛出 BrokenBarrierException 退出本轮次的操作,进入到下一轮次
}
reset 方法
我们知道 parties 用于记录需要到达屏障点的线程数,也即用户创建 CyclicBarrier 时传入的计数,count 值用于记录还有多少线程未到达屏障点,generation 用与表明屏障属于哪一轮次,那么如果需要重复使用 CyclicBarrier ,调用 reset 方法重置 count 和 generation 让其进入新一轮的屏障。
public void reset() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
breakBarrier(); // 终止当前轮次
nextGeneration(); // 开启新的轮次
} finally {
lock.unlock();
}
}
总结:通过上述源码可以看到,只有所有线程都到达屏障点后才属于正常情况,这时会调用 nextGeneration 方法唤醒所有等待线程、复位 count、生成新的 generation。而其他线程可能会因为被中断或超时都会造成本轮次操作的失败,即 generation.broken 被设置为了 true,其它线程检测到该值被设置纷纷抛出 BrokenBarrierException 异常而退出等待。