1 前言
CyclicBarrier是一种同步工具,它允许一组线程在到达一个公共的屏障点时阻塞等待,直到最后一个线程到达屏障点,屏障才能开启,此时所有被阻塞线程才能被唤醒从而继续执行。CyclicBarrier是一个可循环利用(cyclic)的的屏障(barrier),与CountDownLatcher相比的不同之处在于,它可以重置屏障的次数,它可以在释放等待线程之后重新使用。(基于JDK1.8)
2 用法示例
CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
class CyclicBarrierTest {
static CyclicBarrier c = new CyclicBarrier(2);
public static void main(String[] args) {
new Thread(() -> {
try {
System.out.println("子线程到达屏障点");
c.await();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("所有线程均到达屏障点后,子线程打印" + 1);
}).start();
try {
System.out.println("主线程到达屏障点");
c.await();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("所有线程均到达屏障点后,主线程打印" + 2);
}
}
因为主线程和子线程的调度是由CPU决定,所以字符串“所有线程均到达屏障点后,子线程打印1“、”所有线程均到达屏障点后,主线程打印2“的输出先后顺序不固定。但”子(主)线程到达屏障点“ 打印输出一定先于”所有线程均到达屏障点后,子(主)线程打印1(2)“,因为CyclicBarrier规定“只有所有的线程都到达屏障点时,这些被阻塞线程才能继续执行”。
如果将CyclicBarrier的构造方法参数改为“new CyclicBarrier(3)”,字符串“所有线程均到达屏障点后,子(主)线程打印1(2)”一直不会被输出,因为构造方法指定了3个线程,我们实际上只在两个线程中使用了“c.await()”,这样永远不可能有3个线程都达到屏障点的时机,这两个线程将一直被阻塞等待。
另外CyclicBarrier还有一个带有两个参数的构造方法CyclicBarrier(int parties, Runnable barrierAction)
,一个表示屏障拦截的线程数,另一个是Runnable类型参数barrierAction 。此barrierAction 在最后一个线程到达屏障点之后但在唤醒所有线程之前被执行。换句话说,在线程到达屏障时,优先执行barrierAction 。此屏障操作可用在任何一线程继续执行之前更新共享状态。
class CyclicBarrierTest {
static CyclicBarrier c = new CyclicBarrier(2,()->{
String tName= Thread.currentThread().getName();
String gName= Thread.currentThread().getThreadGroup().getName();
System.out.println("Thread '" + tName +"' in thread group '" +gName+"' executes barrier action.");
});
public static void main(String[] args) {
new Thread(() -> {
try {
System.out.println("子线程到达屏障点");
c.await();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("所有线程均到达屏障点后,子线程打印" + 1);
}).start();
try {
System.out.println("主线程到达屏障点");
c.await();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("所有线程均到达屏障点后,主线程打印" + 2);
}
}
可以看出barrierAction先于“xxxxxxx1(2)”输出,再次验证了“在线程到达屏障时,优先执行barrierAction”。
3 成员变量与构造方法
/** The lock for guarding barrier entry */
private final ReentrantLock lock = new ReentrantLock();
/** Condition to wait on until tripped */
private final Condition trip = lock.newCondition();
/** The number of parties */
private final int parties;
/* The command to run when tripped */
private final Runnable barrierCommand;
/** The current generation */
private Generation generation = new Generation();
/**
* Number of parties still waiting. Counts down from parties to 0
* on each generation. It is reset to parties on each new
* generation or when broken.
*/
private int count;
lock
:排他锁,保证数据访问安全。
trip
:与屏障相关的条件。
parties
:屏障拦截的线程数,这个值是常量初始化后不再变化。
barrierCommand
:在所有线程到达屏障点时优先执行的任务。
count
:当前需要阻塞等待的线程数。
generation
:线程的中断状态相关。Generation是一个静态内部类,这只有一个布尔类型的成员变量broken,broken表示线程的中断。
private static class Generation {
boolean broken = false;
}
构造方法CyclicBarrier(int,barrierAction)
主要涉及对各实例变量的初始化,对实例变量count初始为parties,当前没有任何线程被阻塞,所以counts的初值设为屏障拦截的线程数。
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);
}
4 主要方法
(1) await
await方法使当前线程阻塞等待。这里的两个await方法的核心逻辑都委托给行dowait方法,带参的await方法对阻塞时间做了限制,这两个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 {
final ReentrantLock lock = this.lock;
lock.lock();
try {
final Generation g = generation;
if (g.broken)
//generation在初始化时,broken是false,而这之前又没有代码对g.broken更改
//如果出现true,表示其他方法它置为了false,这里抛出异常。
throw new BrokenBarrierException();
if (Thread.interrupted()) {
//如果当前线程是中断的,就执行breakBarrier(记录中断,重置count,并唤醒所有等待线程),
breakBarrier();
throw new InterruptedException();
}
int index = --count;//多一个线程阻塞,将需要阻塞等待的线程数count自减1
//若所有线程均到达屏障点,准备执行command,然后方法返回
if (index == 0) {
// tripped ,
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;//command正常完成,未发生异常
nextGeneration();//生成新的Generation.
return 0;
} finally {
if (!ranAction)//执行command发生异常,执行breakBarrier
breakBarrier();
}
}
//自旋等待,此时线程就被阻塞了。
//只有在 所有线程到达屏障点 或Generation broken或线程中断 或等待超时,才能退出for循环
// loop until tripped, broken, interrupted, or timed out
for (;;) {
try {
if (!timed)
trip.await();//未设置超时就不限时的休眠等待。(同时会释放锁)
else if (nanos > 0L)//设置了超时,当前还未超时,就超时休眠等待。
nanos = trip.awaitNanos(nanos);//(同时会释放锁)
} catch (InterruptedException ie) {
if (g == generation && ! g.broken) {
//generation未被其他线程修改,且未broken就执行breakBarrier
breakBarrier();
throw ie;
} else {
//中断当前线程
// We're about to finish waiting even if we had not
// been interrupted, so this interrupt is deemed to
// "belong" to subsequent execution.
Thread.currentThread().interrupt();
}
}
//从trip.awaitXX中返回了(超时或被trip.signalXX方法唤醒)
if (g.broken)
throw new BrokenBarrierException();
if (g != generation)
//generation被其他线程修改了,表示barrier被重置或所有线程都到达屏障点,
//解除线程的阻塞,方法返回
return index;
if (timed && nanos <= 0L) {
//超时时间已过,执行breakBarrier,然后抛出异常,方法返回
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
dowait的整个方法体被使用锁lock保护起来,保证数据访问安全。
①dowait先检查成员变量generation的状态,如果是broken就抛出BrokenBarrierException异常。Generation是用来记录屏障拦截过程中的线程是否中断、有无其他异常发生、及屏障是否被重置等信息。
② 如果当前线程是中断的(Thread.interrupted()
),就执行Generation.breakBarrier,并抛出InterruptedException。
breakBarrier的主要逻辑是记录中断 、重置count 、并唤醒所有休眠的线程
private void breakBarrier() {
generation.broken = true;//中断
count = parties;//重置count为parties
trip.signalAll();//将所有休眠的线程从Condition.await方法中唤醒返回
}
③将当前需要阻塞等待的线程数count自减。
④若count自减后为0了,表示所有线程均到了达屏障点,就执行command任务(command.run()
),并执行nextGeneration()
产生下一个Genenatrion(方便CyclicBarrier下次重新使用),然后方法返回。。nextGeneration的方法逻辑简单,唤醒所有休眠的线程、重置count、创建一个新的Generation。
private void nextGeneration() {
// signal completion of last generation
trip.signalAll();
// set up next generation
count = parties;
generation = new Generation();
}
若执行command任务过程中发生了异常,就执行breakBarrier()
。
⑤若count自减后不为0了,表示还有线程未到达屏障点,此时需要进入for死循环自旋,从此时起当前线程就被阻塞了。只有当所有线程到达屏障点或Generation broken或线程中断或等待超时,才能退出for循环,方法才能得到返回。
for循环的核心逻辑:
若未设置超时就不限时地休眠等待(trip.await()
);若设置了超时且当前还未超时,就超时休眠等待(nanos = trip.awaitNanos(nanos)
)。
在休眠超时后或被trip.sinaglXXX方法唤醒后,进行一系列的条件检测,确定是否可以退出自旋。
-
如果Generation broken,就抛出异常BrokenBarrierException,方法结束。
-
若 generation被修改了(
g != generation
),表示CyclicBarrier被重置或所有线程都已到达屏障点,就方法返回,解除线程的阻塞状态。 -
若超时时间已过(
timed && nanos <= 0L
),执行breakBarrier,然后抛出异常,方法结束。
若以上三个条件均不满足,就会继续自旋。
(2) getNumberWaiting
getNumberWaiting
方法返回当前正被阻塞等待的线程数。
public int getNumberWaiting() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return parties - count;//屏障拦截的线程总数-当前还需阻塞的线程数
} finally {
lock.unlock();
}
}
(3) isBroken
isBroken查询当前是否broken状态
public boolean isBroken() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return generation.broken;
} finally {
lock.unlock();
}
}
(4) getParties
getParties
方法返回越过屏障所需的线程数。parties是final常量,一经初始化后便不再变化,是一个只读的共享变量,它不存在线程安全问题,不需要用锁来保证数据访问安全。
public int getParties() {
return parties;
}
(5) reset
reset
方法重置barrier的初始状态。如果当前还有线程在屏障点阻塞等待,则有可能抛出BrokenBarrierException异常。这里因为breakBarrier方法将generation.broken置为true,若reset方法还未执行到nextGeneration方法时,此时dowait的for自旋中又恰好检测到generation的broken为true就抛出BrokenBarrierException异常。
public void reset() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
breakBarrier(); // break the current generation
nextGeneration(); // start a new generation
} finally {
lock.unlock();
}
}
5 工作流程
假设一个项目中用代码CyclicBarrier c= new CyclicBarrier(3)
构造一个CyclicBarrier对象, 阻塞等待用的是非超时版本的c.await()
,那么这里c.count的初始值就是3。
①当第一个线程执行到代码片段c.await()
进入到dowait方法中,dowait方法首先要尝试获取锁lock,由于它是第一个线程,此时没有线程竞争能立即获取到锁lock。获取到锁后,将当前需要阻塞等待的线程数count自减1,(count初始为3)此时count自减后为2(不为0),所以它会进入for循环,它一进入for自旋就执行trip.await(),当前(第一个)线程就休眠并释放锁lock .
②当第二个线程执行到代码片段c.await()
进入到dowait方法中,dowait方法首先要尝试获取锁lock,由于第一个线程在休眠后释放了锁lock,所以这个线程也能立即获取到锁。获取到锁后,将当前需要阻塞等待的线程数count自减1,此时count自减后为1,同样不为0,所以它也会进入for循环,它一进入for自旋也立即执行trip.await(),当前(第二个)线程线程就休眠并释放锁lock .
③当第三个线程执行到代码片段c.await()
进入到dowait方法中,dowait方法首先要尝试获取锁lock, 由于第二个线程在休眠后释放了锁lock,所以此线程也能立即获取到锁。获取到锁后,将当前需要阻塞等待的线程数count自减1,此时count自减后为0,方法进入代码块if (index == 0){...}
内部,若有barrierCommand,就先执行barrierCommand任务(由此可见,barrierCommand任务会在最后一个到达屏障点的线程中执行),之后再执行方法nextGeneration()
,然后从dowait方法return返回。可以看出第三个(最后一个到达屏障点的)线程执行到c.await()
不会休眠等待。
④nextGeneration()
方法很关键,此方法体中的trip.signalAll()
将唤醒前两个(所有)线程,使得前两个线程从trip.await()
的休眠中返回,继续执行for循环接下来的代码。在接下来的for循环代码中检测到if (g != generation)
条件成立(nextGeneration方法将重新创建一个Generation对象,并将引用赋给成员变量generation),从而从dowait方法中return返回,结束阻塞状态,这两个线程得以继续执行。
参考: 《Java并发编程的艺术》方腾飞