什么是CyclicBarrier?
CyclicBarrie和上一篇中讲到CountDownLatch很类似,它能阻塞一组线程直到某个事件的发生。
栅栏与闭锁的关键区别在于:所有必须同时到达栅栏位置才能够继续执行。也就是闭锁用于等待某个事件,栅栏用于等待其它线程
CyclicBarrier的基本过程
CyclicBarrier可以使一定数量的线程反复的在栅栏处汇集。
- 当线程到达栅栏位置时将调用
await
方法,直到所有方法都到达栅栏位置 - 当所有线程都到达栅栏位置后,那么栅栏将打开,所有的线程将被释放
- 栅栏被释放后会执行barrierAction的runable,然后重置计数器
CyclicBarrier应用示例
import java.util.concurrent.CyclicBarrier;
/**
* 同学们去春游
*
* 首先:同学们都先上公司门口的大巴。人齐了之后,巴士出发。
* 其次:所有巴士都到达景点后,大家集合,开始春游。
*/
public class CyclicBarrierDemo {
private static final int NUMS = 5;
public static final CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMS, new Master());
public static void main(String[] args) {
for (int i = 0; i < NUMS; i++) {
Thread thread = new Thread(new Student(i, cyclicBarrier));
thread.start();
}
}
}
class Student implements Runnable {
private CyclicBarrier cyclicBarrier;
private volatile Integer studenNo = 0;
public Student(Integer studenNo, CyclicBarrier cyclicBarrier) {
this.studenNo = studenNo;
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
try {
System.out.println("学生" + studenNo + ", 已经上巴士。");
cyclicBarrier.await();
System.out.println("学生" + studenNo + ", 巴士已经到达目的地。");
cyclicBarrier.await();
} catch (Throwable e) {
e.printStackTrace();
}
}
}
class Master implements Runnable {
private static int step = 1;
@Override
public void run() {
if (step == 1) {
System.out.println("同学们都已经上大巴了,咱们出发!");
} else if (step == 2) {
System.out.println("所有大巴都到了,同学们开始春游!");
}
step++;
}
}
示例中所有线程会在栅栏中集结两次,一次是所有同学上大巴;第二次是所有大巴都到达目的地。
CyclicBarrier源码解析
从上面的示例,主要有如下关键方法:
- 构造方法:
CyclicBarrier(NUMS, new Master());
- 阻塞方法:
cyclicBarrier.await();
构造方法:CyclicBarrier(NUMS, new Master())
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
public CyclicBarrier(int parties) {
this(parties, null);
}
CyclicBarrier有两个构造函数,其中:
- parties:表示屏障拦截的线程数量,例子中为5
- barrierAction:表示在达到拦截的线程数量后执行barrierAction,然后恢复阻塞的线程执行
阻塞方法:cyclicBarrier.await()
public int await() throws InterruptedException, BrokenBarrierException {
try {
// 不超时等待
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
public int await(long timeout, TimeUnit unit)
throws InterruptedException,
BrokenBarrierException,
TimeoutException {
return dowait(true, unit.toNanos(timeout));
}
两个方法的区别在于第二个可以传入超时参数,默认是不超时,它们都调用dowait
方法,代码如下:
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
// 使用独占锁来执行dowait方法,并发性可能不是很高
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 {
// 如果执行栅栏任务的时候失败了,就将损坏状态设置为true
if (!ranAction)
breakBarrier();
}
}
// 自旋直到触发、broken、中断或超时
for (; ; ) {
try {
// 如果没有时间限制,则直接等待,直到被唤醒
if (!timed)
trip.await();
else if (nanos > 0L) // 如果有时间限制,则等待指定时间
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
// 当前代没有损坏
if (g == generation && !g.broken) {
breakBarrier(); // 让栅栏失效
throw ie;
} else {
// 上面条件不满足,说明这个线程不是这代的, 就不会影响当前这代栅栏的执行,所以,就打个中断标记
Thread.currentThread().interrupt();
}
}
// 当有任何一个线程中断了,就会调用breakBarrier方法,就会唤醒其他的线程,其他线程醒来后,也要抛出异常
if (g.broken)
throw new BrokenBarrierException();
// g != generation表示正常换代了,返回当前线程所在栅栏的下标
// 如果 g == generation,说明还没有换代,那为什么会醒了?
// 因为一个线程可以使用多个栅栏,当别的栅栏唤醒了这个线程,就会走到这里,所以需要判断是否是当前代。
// 正是因为这个原因,才需要generation来保证正确。
if (g != generation)
return index;
// 如果有时间限制,且时间小于等于0,销毁栅栏并抛出超时异常
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
基本流程如下:
- 执行一些验证:栅栏是否broken、线程是否中断
- 如果是最后一个线程调用dowait,则执行栅栏任务barrierAction,然后更新代
nextGeneration
private void nextGeneration() {
// 唤醒所有阻塞线程
trip.signalAll();
// 重置count
count = parties;
// 重新生成下一代
generation = new Generation();
}
- 如果不是最后一个线程调用dowait,则自旋,
trip.await()
会进行阻塞,直至发生如下情况才会被唤醒或终止:
- 最后一个线程到达,即index==0
- 某个参与栅栏的线程等待超时
- 某个参与栅栏的线程被中断
- 调用了CyclicBarrier的reset()方法,该方法会将屏障置为初始状态
- 在被唤醒之后,栅栏没有损坏且是同一代,则返回下标index
在barrier损坏或有一个线程被中断,会调用breakBarrier方法来终止所有线程
private void breakBarrier() {
// 设置broken为true,自旋的线程会抛出BrokenBarrierException异常
generation.broken = true;
count = parties;
// 唤醒所有线程,由于broken为true,所有都会被终止并抛出异常
trip.signalAll();
}
至此,await()方法源码全部解析完毕
CyclicBarrier和CountDownLatch的区别
- CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置或自动重置,可以使用多次,所以CyclicBarrier能够处理更为复杂的场景;
- CyclicBarrier还提供了一些其他有用的方法,比如getNumberWaiting()方法可以获得CyclicBarrier阻塞的线程数量,isBroken()方法用来了解阻塞的线程是否被中断;
- CountDownLatch允许一个或多个线程等待一组事件的产生,而CyclicBarrier用于等待其他线程运行到栅栏位置。
思考
CyclicBarrier有没有什么不足?
有,在dowait方法中,我们可以发现它调用的是ReentrantLock独占锁
的方式来实现多线程并发,在并发量大的情况下性能可能不是很高
为什么要有代的区分?
因为线程在等待唤醒的过程中,如果线程被其它的栅栏唤醒,但不是同一个栅栏,也就是不同一个代,可以通过代来判断是不是同一个代,然后区分是正常结束返回下标还是继续自旋等待
在换代nextGeneration的过程中,如果某个线程中断会怎么样?
不影响,正常结束。JDK中认为任务已完成,就不会在乎中断,但是会打个中断标记,在dowait方法中有注释java Thread.currentThread().interrupt();