一 前言
前面我们学了一次性的闭锁CountDownLatch,下面我们会接触到一个可以循环的同步辅助类:CyclicBarrier,它允许一组线程互相等待,直到到达某个公共屏障点,在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待。
CyclicBarrier类似于CountDownLatch的地方是它本身也是个计数器,用以计数的变量则是初始化时传入的变量count
。不同点则是CyclicBarrier的计数数的是调用了CyclicBarrier.await()
而进入等待的线程数,CountDownLatch的计数数则是初始计数值与调用了CountDownLatch.await()
而导致的差值。同时前者可以进行循环操作,当一组计数结束后则会开始新一轮的计数,不断循环。CyclicBarrier初始时还可带一个Runnable的参数,此Runnable任务在CyclicBarrier的数目达到后,会在其它线程被唤醒前被执行。
CyclicBarrier的所有API如下:
- public CyclicBarrier(int parties) 创建一个新的
CyclicBarrier
,它将在给定数量的参与者(线程)处于等待状态时启动,但它不会在启动 barrier 时执行预定义的操作。 - public CyclicBarrier(int parties, Runnable barrierAction) 创建一个新的
CyclicBarrier
,它将在给定数量的参与者(线程)处于等待状态时启动,并在启动 barrier 时执行给定的屏障操作,该操作由最后一个进入 barrier 的线程执行。 - public int await() throws InterruptedException, BrokenBarrierException 在所有参与者都已经在此 barrier 上调用
await
方法之前,将一直等待。 - public int await(long timeout,TimeUnit unit) throws InterruptedException, BrokenBarrierException,TimeoutException 在所有参与者都已经在此屏障上调用 await 方法之前将一直等待,或者超出了指定的等待时间。
- public int getNumberWaiting() 返回当前在屏障处等待的参与者数目。此方法主要用于调试和断言。
- public int getParties() 返回要求启动此 barrier 的参与者数目。
- public boolean isBroken() 查询此屏障是否处于损坏状态。
- public void reset() 将屏障重置为其初始状态。
CyclicBarrier实现主要基于ReentrantLock,不熟悉的可以先看下ReentrantLock的解析。
国际惯例,先看下基本的东西:
一些参数:
// 用来判断这一代的屏障有没有被broken
private static class Generation {
boolean broken = false;
}
// 使用独占锁保护屏障入口
private final ReentrantLock lock = new ReentrantLock();
// 使用Condition组成等待队列
private final Condition trip = lock.newCondition();
// 记录参与等待的线程数
private final int parties;
// 当所有线程到达屏障点之后,首先执行的命令
private final Runnable barrierCommand;
private Generation generation = new Generation();
// 仍需要等待加入的线程数,每一代都会从parties降为0
// 每次到新一代的时候这个值都会被重新设置为parties
private int count;
Generation用来控制屏障的循环使用,如果generation.broken为true的话,说明这个屏障已经损坏,当某个线程await的时候,直接抛出异常
构造函数:
// 初始化屏障的阀值
// 当最后一个线程进入屏障时不会执行任务
public CyclicBarrier(int parties) {
this(parties, null);
}
// 初始化屏障的阀值
// 当最后一个线程进入屏障时所执行的Runnable任务
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
二 await()与await(long, TimeUnit)
首先是不限时的版本:
// 将线程挂起直到挂起的线程数量达到paties
public int await() throws InterruptedException, BrokenBarrierException {
try {
// 调用了dowait(boolean, long)方法,表明不使用限时
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
只有一行代码,即调用了dowait(long, TimeUnit)
方法。
下面看一下限时版本与前者的区别:
// 希望在规定时间内被执行的await()
public int await(long timeout, TimeUnit unit)
throws InterruptedException,
BrokenBarrierException,
TimeoutException {
return dowait(true, unit.toNanos(timeout));
}
调用的方法与前者是同一个,只不过多了一个时间的参数。下面跳入这个方法:
// 返回还需要多少个等待线程才能够唤醒所有
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
// 使用独占锁,每条线程lock()和unlock()之间的代码
final ReentrantLock lock = this.lock;
lock.lock();
try {
final Generation g = generation;
// 若generation已被broken,说明其他等待的线程被中断
if (g.broken)
throw new BrokenBarrierException();
// 该线程已被中断
if (Thread.interrupted()) {
// 将当前屏障的一代设为broken并且唤醒所有正在等待的线程
breakBarrier();
throw new InterruptedException();
}
int index = --count;
// 到达屏障的阀值,执行之前已设定的任务
if (index == 0) { // tripped
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
// 更新屏障初始的状态位为paties的值并且将所有Condition队列的结点转换到Sync队列
// 为唤醒所有处于休眠状态的线程做准备工作
// 需要注意的是,唤醒所有阻塞线程不是在这里
nextGeneration();
return 0;
} finally {
// 发生错误,将当前屏障的一代设为broken并且唤醒所有正在等待的线程
if (!ranAction)
breakBarrier();
}
}
// loop until tripped, broken, interrupted, or timed out
for (;;) {
try {
// 放入Condition等待队列
if (!timed)
trip.await();
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
// 中断后若当前屏障还没有被broken,则唤醒所有结点
if (g == generation && ! g.broken) {
breakBarrier();
throw ie;
} else {
Thread.currentThread().interrupt();
}
}
//TODO 这里没怎么看懂啊!!!
// 当前屏障已被broken
if (g.broken)
throw new BrokenBarrierException();
if (g != generation)
return index;
// 将当前屏障的一代设为broken并且唤醒所有正在等待的线程,并抛出超时错误
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
// 解开独占锁
lock.unlock();
}
}
这个是整个CyclicBarrier的核心代码,看懂这个基本就看懂了CyclicBarrier类。
就着上面的注释来讲下具体的步骤:
- 使用ReentrantLock独占锁锁定所有代码
- 若本次到达屏障的线程存在已被broken的,则抛出BrokenBarrierException异常。(这里不用唤醒所有线程的原因是在broken当前屏障的方法中存在唤醒所有线程的方法)
- 若本线程已经被中断,则将当前屏障设置为broken,并且唤醒所有线程
- 若调用
await()
方法的线程数量已达到设置的paties
,则将所有在Condition队列中的等待线程转移到Sync队列中,唤醒线程则不在此处实现。(AQS会按照Sync队列顺序依次唤醒线程) - 到此处时说明:此时线程的数量还没有到达设置的阀值。因此会利用循环挂起,直到达到阀值、broken、中断、超时。
此处的方法基本都是调用Condition以及ReentrantLock里面的方法,有兴趣的可以看一下以前的文章。
上面使用到的CyclicBarrier实现的方法则有:
- breakBarrier():将当前屏障的一代设为broken并且唤醒所有正在等待的线程
- nextGeneration():更新屏障初始的状态位为paties的值并且将所有Condition队列的结点转换到Sync队列
下面看下:
// 只能被当前持有锁的线程调用
// 将当前屏障的一代设为broken并且唤醒所有正在等待的线程
private void breakBarrier() {
generation.broken = true;
count = parties;
// Condition的signalAll()方法,唤醒所有正在等待的线程
trip.signalAll();
}
// 只能被当前持有锁的线程调用
// 更新屏障初始的状态位并且唤醒所有正在等待的线程
private void nextGeneration() {
// 更新屏障初始的状态位为paties的值并且将所有Condition队列的结点转换到Sync队列
trip.signalAll();
// 重置count为paties
count = parties;
// 设置generation状态为false
generation = new Generation();
}
三 其他
// 查询此屏障是否处于断开状态。
public boolean isBroken() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return generation.broken;
} finally {
lock.unlock();
}
}
// 重新设置屏障,将会唤醒之前的所有的等待线程
public void reset() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 此处可以思考一下为什么两个差不多功能的方法要连续调用
breakBarrier(); // break the current generation
nextGeneration(); // start a new generation
} finally {
lock.unlock();
}
}
// 得到当前正在等待的线程数量
public int getNumberWaiting() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return parties - count;
} finally {
lock.unlock();
}
}
public int getParties() {
return parties;
}
四 总结
总的来说,CyclicBarrier是基于ReentrantLock与Condition这两个类来实现的,通过ReentrantLock几乎对所有方法的全部字段进行加锁来保证CyclicBarrier的安全性,同时利用Condition将未达到阀值的线程加入等待队列中。 相比于CountDownLatch利用CAS的方法来实现应该会消耗更多的性能。不过CyclicBarrier相对于CountDownLatch的优势也是有的,可以不断循环利用的generation
以及当处于休眠线程的数量达到阀值时会先执行预先定义好的任务。
CyclicBarrier 如果在await
时因为中断、失败、超时等原因提前离开了屏障点,那么任务组中的其他任务将立即被中断,以InterruptedException异常离开线程。