写在开头
面试官:同学,AQS的原理知道吗?
我:学过一点,抽象队列同步器,Java中很多同步工具都是基于它的...
面试官:好的,那其中CyclicBarrier学过吗?讲一讲它的妙用吧
我:啊,这个,这个我平时写代码没用过...
面试官:那你回去再学学吧!
随着Java的国内竞争环境逐渐激烈,面试时遇到很多奇葩的问题也是越来越多,以上是模拟的一个面试场景,同学们看下你们能答得上来不?😝
什么是CyclicBarrier?
在过去的几天里,我们基于AQS学习了不少内容,其中基于AQS构建的同步工具类也学了Semaphore(信号量)和CountDownLatch(倒计时器),甚至于也手撕过同步器,今天我们继续来学习另外一个同步类:CyclicBarrier
CyclicBarrier(循环屏障):让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
CyclicBarrier的原理
在CyclicBarrier有两个成员变量分别为parties
,count
,前者代表每次拦截的线程数量,后者是初始化时保持和parties相等的计数标识,每有一个线程执行到同步点时,count减1,当count值变为0时说明所有线程都走到了同步点,这时就可以尝试执行我们在构造方法中设计的任务啦。
【源码解析1】
//每次拦截的线程数 private final int parties; //计数器 private int count; //一个参数的构造 public CyclicBarrier(int parties) { this(parties, null); } //多参构造,parties为拦截线程数,barrierAction这个 Runnable会在 CyclicBarrier 的计数器为 0 的时候执行,用来完成更复杂的任务。 public CyclicBarrier(int parties, Runnable barrierAction) { if (parties <= 0) throw new IllegalArgumentException(); this.parties = parties; this.count = parties; this.barrierCommand = barrierAction; }
每个线程通过调用await方法告诉CyclicBarrier已经到达屏障,然后进行阻塞等待,知道count等于0,所有线程都到达了屏障,因此,我们跟入await方法的源码中去看一下。
【源码解析2】
public int await() throws InterruptedException, BrokenBarrierException { try { return dowait(false, 0L); } catch (TimeoutException toe) { throw new Error(toe); // cannot happen } } //await方法内部,继续调用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) throw new BrokenBarrierException(); // 如果线程中断了,抛出异常 if (Thread.interrupted()) { //打破屏障 breakBarrier(); throw new InterruptedException(); } // cout减1 int index = --count; // 当 count 数量减为 0 之后说明最后一个线程已经到达栅栏了,也就是达到了可以执行await 方法之后的条件 if (index == 0) { // tripped boolean ranAction = false; try { final Runnable command = barrierCommand; if (command != null) command.run(); ranAction = true; // 将 count 重置为 parties 属性的初始化值 // 唤醒之前等待的线程 // 下一波执行开始 nextGeneration(); return 0; } finally { if (!ranAction) breakBarrier(); } } // 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) { 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(); } } if (g.broken) throw new BrokenBarrierException(); if (g != generation) return index; if (timed && nanos <= 0L) { breakBarrier(); throw new TimeoutException(); } } } finally { lock.unlock(); } }
在dowait(boolean timed, long nanos),可以通过时间参数来设置阻塞的时间,默认为false,在这个方法内部,每次线程调用await后,都会进行--count操作,直到index为0时,会去执行command,然后唤醒线程继续向下执行,CyclicBarrier 的计数器可以通过reset()方法重置,所以它能处理循环使用的场景。
CyclicBarrier的使用
大致的了解了CyclicBarrier的原理之后,我们写个小demo测试一下它如何使用
【代码示例】
public class Test { public static void main(String[] args) { int numberOfThreads = 3; // 线程数量 CyclicBarrier barrier = new CyclicBarrier(numberOfThreads, () -> { // 当所有线程都到达障碍点时执行的操作 System.out.println("所有线程都已到达屏障,进入下一阶段"); }); for (int i = 0; i < numberOfThreads; i++) { new Thread(new Task(barrier), "Thread " + (i + 1)).start(); } } static class Task implements Runnable { private final CyclicBarrier barrier; public Task(CyclicBarrier barrier) { this.barrier = barrier; } @Override public void run() { try { System.out.println(Thread.currentThread().getName() + " 正在屏障处等待"); barrier.await(); // 等待所有线程到达障碍点 System.out.println(Thread.currentThread().getName() + " 已越过屏障."); } catch (Exception e) { e.printStackTrace(); } } } }
输出:
Thread 2 正在屏障处等待 Thread 1 正在屏障处等待 Thread 3 正在屏障处等待 所有线程都已到达屏障,进入下一阶段 Thread 3 已越过屏障. Thread 1 已越过屏障. Thread 2 已越过屏障.
结尾彩蛋
如果本篇博客对您有一定的帮助,大家记得留言+点赞+收藏呀。原创不易,转载请联系Build哥!
如果您想与Build哥的关系更近一步,还可以关注“JavaBuild888”,在这里除了看到《Java成长计划》系列博文,还有提升工作效率的小笔记、读书心得、大厂面经、人生感悟等等,欢迎您的加入!