CyclicBarrier 是一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。
在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。
因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier。
CyclicBarrier 支持一个可选的 Runnable 命令,在一组线程中的最后一个线程到达之后(但在释放所有线程之前),该命令只在每个屏障点运行一次。若在继续所有参与线程之前更新共享状态,此屏障操作很有用。
一、使用示例
比如统计文件行数,多个线程,最终都读完加一个总和。
public class TestBarrier {
private static CyclicBarrier barrier;
public static class CountThread extends Thread {
@Override
public void run() {
// 业务逻辑
System.out.println("线程" + Thread.currentThread().getName() + "读完了!");
// 等待其它线程
try {
barrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
barrier = new CyclicBarrier(10, new Runnable() {
@Override
public void run() {
System.out.println("所有线程都读完了,可以合计总数了");
// 业务逻辑
}
});
for(int i=0; i<10; i++){
new CountThread().start();
}
}
}
运行结果:
线程Thread-2读完了!
线程Thread-0读完了!
线程Thread-1读完了!
线程Thread-3读完了!
线程Thread-4读完了!
线程Thread-6读完了!
线程Thread-5读完了!
线程Thread-7读完了!
线程Thread-8读完了!
线程Thread-9读完了!
所有线程都读完了,可以合计总数了
二、源码分析
CyclicBarrier 是基于重入锁 ReentrantLock 实现相关逻辑的。所以要弄懂 CyclicBarrier 的源码,仅需有 ReentrantLock 相关的背景知识即可
2.1 构造方法及成员变量
CyclicBarrier 包含两个有参构造方法,分别如下:
public class CyclicBarrier {
private final ReentrantLock lock = new ReentrantLock();
private final Condition trip = lock.newCondition();
private final int parties;
private final Runnable barrierCommand;
/**
* CyclicBarrier 是可循环使用的屏障,这里使用 Generation 记录当前轮次 CyclicBarrier
* 的运行状态。当所有线程到达屏障后,generation 将会被更新,表示 CyclicBarrier 进入新一
* 轮的运行轮次中。
*/
private Generation generation = new Generation();
private static class Generation {
boolean broken; // initially false
}
public class CyclicBarrier {
/** 创建一个允许 parties 个线程通行的屏障 */
public CyclicBarrier(int parties) {
this(parties, null);
}
/**
* 创建一个允许 parties 个线程通行的屏障,若 barrierAction 回调对象不为 null,
* 则在最后一个线程到达屏障后,执行相应的回调逻辑
*/
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
}
上面的第二个构造方法初始化了一些成员变量,下面我们就来说明一下这些成员变量的作用。
成员变量 | 作用 |
---|---|
parties | 线程数,即当 parties 个线程到达屏障后,屏障才会放行 |
count | 计数器,当 count > 0 时,到达屏障的线程会进入等待状态。当最后一个线程到达屏障后,count 自减至0。最后一个到达的线程会执行回调方法,并唤醒其他处于等待状态中的线程。 |
barrierCommand | 回调对象,如果不为 null,会在第 parties 个线程到达屏障后被执行 |
2.2 await
基本过程:
- 如果当前线程不是最后一个线程,调用该方法,那么就在这里一直等待;
- 直到最后一个线程调用该方法,执行附加指令后,通知所有其他线程。
public int await() throws InterruptedException, BrokenBarrierException {
try {
// await 的逻辑封装在 dowait 中
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 {
final Generation g = generation;
// 如果 g.broken = true,表明屏障被破坏了,这里直接抛出异常
if (g.broken)
throw new BrokenBarrierException();
// 如果线程中断,则调用 breakBarrier 破坏屏障
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
/*
* index 表示线程到达屏障的顺序,index = parties - 1 表明当前线程是第一个
* 到达屏障的。index = 0,表明当前线程是最有一个到达屏障的。
*/
int index = --count;
// 当 index = 0 时,唤醒所有处于等待状态的线程
if (index == 0) { // tripped
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
// 如果回调对象不为 null,则执行回调
if (command != null)
command.run();
ranAction = true;
// 重置屏障状态,使其进入新一轮的运行过程中
nextGeneration();
return 0;
} finally {
// 若执行回调的过程中发生异常,此时调用 breakBarrier 破坏屏障
if (!ranAction)
breakBarrier();
}
}
// 线程运行到此处的线程都会被屏障挡住,并进入等待状态。
for (;;) {
try {
if (!timed)
trip.await();
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
/*
* 若下面的条件成立,则表明本轮运行还未结束。此时调用 breakBarrier
* 破坏屏障,唤醒其他线程,并抛出异常
*/
if (g == generation && ! g.broken) {
breakBarrier();
throw ie;
} else {
/*
* 若上面的条件不成立,则有两种可能:
* 1. g != generation
* 此种情况下,表明循环屏障的第 g 轮次的运行已经结束,屏障已经
* 进入了新的一轮运行轮次中。当前线程在稍后返回 到达屏障 的顺序即可
*
* 2. g = generation 但 g.broken = true
* 此种情况下,表明已经有线程执行过 breakBarrier 方法了,当前
* 线程则会在稍后抛出 BrokenBarrierException
*/
Thread.currentThread().interrupt();
}
}
// 屏障被破坏,则抛出 BrokenBarrierException 异常
if (g.broken)
throw new BrokenBarrierException();
// 屏障进入新的运行轮次,此时返回线程在上一轮次到达屏障的顺序
if (g != generation)
return index;
// 超时判断
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
/** 开启新的一轮运行过程 */
private void nextGeneration() {
// 【唤醒所有处于等待状态中的线程】
trip.signalAll();
// 重置 count
count = parties;
// 重新创建 Generation,表明进入循环屏障进入新的一轮运行轮次中
generation = new Generation();
}
/** 破坏屏障 */
private void breakBarrier() {
// 设置屏障是否被破坏标志
generation.broken = true;
// 重置 count
count = parties;
// 唤醒所有处于等待状态中的线程
trip.signalAll();
}
2.3 reset
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();
}
}
三、CountDownLatch 和 CyclicBarrier
CountDownLatch 允许一个或一组线程等待其他线程完成后再恢复运行。线程可通过调用await方法进入等待状态,在其他线程调用countDown方法将计数器减为0后,处于等待状态的线程即可恢复运行。
CyclicBarrier (可循环使用的屏障)则与此不同,CyclicBarrier 允许一组线程到达屏障后阻塞住,直到最后一个线程进入到达屏障,所有线程才恢复运行。
它们之间主要的区别在于唤醒等待线程的时机:
CountDownLatch 是在计数器减为0后,唤醒等待线程。CyclicBarrier 是在计数器(等待线程数)增长到指定数量后,再唤醒等待线程
差异点 | CountDownLatch | CyclicBarrier |
---|---|---|
是否可循环使用 | 否 | 是 |
是否可设置回调 | 否 | 是 |