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并发编程的艺术》方腾飞