封面:AQS家族的“外门弟子”:CyclicBarrier.png

王有志,一个分享硬核Java技术的互金摸鱼侠
加入Java人的提桶跑路群:共同富裕的Java人

今天我们来学习AQS家族的“外门弟子”:CyclicBarrier。
为什么说CyclicBarrier是AQS家族的“外门弟子”呢?那是因为CyclicBarrier自身和内部类Generation并没有继承AQS,但在源码的实现中却深度依赖AQS家族的成员ReentrantLock。就像修仙小说中,大家族会区分外门和内门,外门弟子通常会借助内门弟子的名声行事,CyclicBarrier正是这样,因此算是AQS家族的“外门弟子”。在实际的面试中,CyclicBarrier的出现的次数较少,通常会出现在与CountDownLatch比较的问题当中
今天我们就逐步拆解CyclicBarrier,来看看它与CountDownLatch之间到底有什么差别。

CyclicBarrier是什么?

先从CyclicBarrier的名字开始入手,Cyclic是形容词,译为“循环的,周期的”,Barrier是名词,译为“屏障,栅栏”,组合起来就是“循环的屏障”,那么该怎么理解“循环的屏障”呢?我们来看CyclicBarrier的注释是怎么解释的:

A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point. CyclicBarrier是一种同步辅助工具,允许一组线程等待彼此到达共同的屏障点。

The barrier is called cyclic because it can be re-used after the waiting threads are released. 因为在等待线程释放后可以重复使用,所以屏障被称为循环屏障。
看起来与CountDownLatch有些相似,我们通过一张图来展示下CyclicBarrier是怎样工作的:
图1:CyclicBarrier的作用.gif
部分线程到达屏障后,会在屏障处等待,只有全部线程都到达屏障后,才会继续执行。如果以CountDownLatch中越野徒步来举例的话,把老板拿掉,选手之间的互相等待,就是CyclicBarrier了。
另外,注释中说CyclicBarrier是“re-used”,即可重复使用的。回想一下CountDownLatch的实现,并未做任何重置计数器的工作,即当CountDownLatch的计数减为0后不能恢复,也就是说CountDownLatch的功能是一次性的
Tips:实际上,可以用CountDownLatch实现类似于CyclicBarrier的功能。

CyclicBarrier怎么用?

我们用没有老板参加的越野徒步来举例,部分先到的选手要等待后到的选手一起吃午饭,用CyclicBarrier来实现的代码是这样的:

  1. // 初始化CyclicBarrier
  2. CyclicBarrier cyclicBarrier = new CyclicBarrier(10);
  3. for (int i = 0; i < 10; i++) {
  4. int finalI = i;
  5. new Thread(() -> {
  6. try {
  7. TimeUnit.SECONDS.sleep((finalI + 1));
  8. } catch (InterruptedException e) {
  9. throw new RuntimeException(e);
  10. }
  11. try {
  12. System.out.println("选手[" + finalI + "]到达终点,等待其他选手!!!");
  13. // 线程在屏障点处等待
  14. cyclicBarrier.await();
  15. System.out.println("选手[" + finalI + "]开始吃午饭啦!!!");
  16. } catch (InterruptedException | BrokenBarrierException e) {
  17. throw new RuntimeException(e);
  18. }
  19. }).start();
  20. }

用法和CountDownLatch很相似,构造函数设置CyclicBarrier需要多少个线程达到屏障后统一行动,区别是CyclicBarrier在每个线程中都调用了CyclicBarrier#await,而我们在使用CountDownLatch时只在主线程中调用了一次CountDownLatch#await
那CountDownLatch可以在线程中调用CountDownLatch#await吗?答案是可以的,这样使用的效果和CyclicBarrier是一样的:

  1. CountDownLatch countDownLatch = new CountDownLatch(10);
  2. for (int i = 0; i < 10; i++) {
  3. int finalI = i;
  4. new Thread(() -> {
  5. try {
  6. TimeUnit.SECONDS.sleep((finalI + 1));
  7. } catch (InterruptedException e) {
  8. throw new RuntimeException(e);
  9. }
  10. System.out.println("选手[" + finalI + "]到达终点!!!");
  11. countDownLatch.countDown();
  12. try {
  13. countDownLatch.await();
  14. System.out.println("选手[" + finalI + "]开始吃午饭啦!!!");
  15. } catch (InterruptedException e) {
  16. throw new RuntimeException(e);
  17. }
  18. }).start();
  19. }

通过上面的例子,我们不难想到CyclicBarrier#await方法是同时具备了CountDownLatch#countDown方法和CountDownLatch#await方法的能力,即执行了计数减1,又执行了暂停线程

CyclicBarrier是怎么实现的?

我们先整体认识一下CyclicBarrier:
图2:CyclicBarrier.png
CyclicBarrier的内部结构比CountDownLatch复杂一些,除了我们前面提到的借助AQS的“内门弟子”ReentrantLock类型的lock和Condition类型的trip外,CyclicBarrier还有两个“特别”的地方:

  • 内部类Generation,直译过来是“代”,它起到什么作用?
  • Runnable类型的成员变量barrierCommand,它又做了些什么?

其余的部分,大部分可以在CountDownLatch中找到对应的方法,或者通过名称我们就很容易得知它们的作用。

CyclicBarrier的构造方法

CyclicBarrier提供了两个(实际是一个)构造方法:

  1. // 需要到达屏障的线程数
  2. private final int parties;
  3. // 所有线程都到达后执行的动作
  4. private final Runnable barrierCommand;
  5. // 计数器
  6. private int count;
  7. public CyclicBarrier(int parties) {
  8. this(parties, null);
  9. }
  10. public CyclicBarrier(int parties, Runnable barrierAction) {
  11. if (parties <= 0) {
  12. throw new IllegalArgumentException();
  13. }
  14. this.parties = parties;
  15. this.count = parties;
  16. this.barrierCommand = barrierAction;
  17. }

第二个构造函数接收了两个参数:

  • parties:表示需要多少个线程到达屏障处调用CyclicBarrier#await
  • barrierAction:所有线程到达屏障后执行的动作。

构造方法的代码一如既往的简单,只有一处比较容易产生疑惑,parties和count有什么区别?
首先来看成员变量的声明,parties使用了final,表明它是不可变的对象,代表CyclicBarrier需要几个线程共同到达屏障处;而count是计数器,初始值是parties,随着到达屏障处的线程数量增多count会逐步减少至0。

CyclicBarrier的内部类Generation

  1. private static class Generation {
  2. Generation() {}
  3. boolean broken;
  4. }

Generation用于标记CyclicBarrier的当前代,Doug Lea是这么解释它的作用的:

Each use of the barrier is represented as a generation instance. The generation changes whenever the barrier is tripped, or is reset. 每次使用屏障(CyclicBarrier)都需要一个Generation实例。无论是通过屏障还是重置屏障,Generation都会发生改变。

Generation中的broken用于标记当前的CyclicBarrier是否被打破,默认为false,值为true时表示当前CyclicBarrier已经被打破,此时CyclicBarrier不能正常使用,需要调用CyclicBarrier#reset方法重置CyclicBarrier的状态。

CyclicBarrier#await方法

前面我们猜测CyclicBarrier#await方法即实现了计数减1,又实现了线程等待的功能,下面我们就通过源码来验证我们的想法:

  1. public int await() throws InterruptedException, BrokenBarrierException {
  2. try {
  3. return dowait(false, 0L);
  4. } catch (TimeoutException toe) {
  5. throw new Error(toe);
  6. }
  7. }
  8. public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException {
  9. return dowait(true, unit.toNanos(timeout));
  10. }

两个重载方法都指向了CyclicBarrier#dowait方法:

  1. private int dowait(boolean timed, long nanos) throws InterruptedException, BrokenBarrierException, TimeoutException {
  2. // 使用ReentrantLock
  3. final ReentrantLock lock = this.lock;
  4. lock.lock();
  5. try {
  6. // 第2部分
  7. // 获取CyclicBarrier的当前代,并检查CyclicBarrier是否被打破
  8. final Generation g = generation;
  9. if (g.broken) {
  10. throw new BrokenBarrierException();
  11. }
  12. // 线程被中断时,调用breakBarrier方法
  13. if (Thread.interrupted()) {
  14. breakBarrier();
  15. throw new InterruptedException();
  16. }
  17. // 第3部分
  18. //计数器减1
  19. int index = --count;
  20. // 计数器为0时表示所有线程都到达了,此时要做的就是唤醒等待中的线程
  21. if (index == 0) {
  22. boolean ranAction = false;
  23. try {
  24. // 执行唤醒前的操作
  25. final Runnable command = barrierCommand;
  26. if (command != null) {
  27. command.run();
  28. }
  29. ranAction = true;
  30. // CyclicBarrier进入下一代
  31. nextGeneration();
  32. return 0;
  33. } finally {
  34. if (!ranAction) {
  35. breakBarrier();
  36. }
  37. }
  38. }
  39. // 第4部分
  40. // 只有部分线程到达屏障处的情况
  41. for (;;) {
  42. try {
  43. //调用等待逻辑)
  44. if (!timed) {
  45. trip.await();
  46. } else if (nanos > 0L) {
  47. nanos = trip.awaitNanos(nanos);
  48. }
  49. } catch (InterruptedException ie) {
  50. // 线程被中断时,调用breakBarrier方法
  51. if (g == generation && ! g.broken) {
  52. breakBarrier();
  53. throw ie;
  54. } else {
  55. Thread.currentThread().interrupt();
  56. }
  57. }
  58. if (g.broken) {
  59. throw new BrokenBarrierException();
  60. }
  61. // 如果不是当前代,返回计数器的值
  62. if (g != generation) {
  63. return index;
  64. }
  65. // 如果等待超时,调用breakBarrier方法
  66. if (timed && nanos <= 0L) {
  67. breakBarrier();
  68. throw new TimeoutException();
  69. }
  70. }
  71. } finally {
  72. lock.unlock();
  73. }
  74. }

CyclicBarrier#dowait方法看起来很长,但如果拆成3部分来看逻辑并不复杂:

  • 第1部分:CyclicBarrier与线程的状态校验;
  • 第2部分:当计数器减1后值为0时,唤醒所有等待中的线程;
  • 第3部分:当计数器减1后值不为0时,线程进入等待状态。

先来看第1部分,CyclicBarrier与线程的状态校验的部分,先是判断CyclicBarrier是否被打破,接着判断当前线程是否为中断状态,如果是则调用CyclicBarrier#breakBarrier方法:

  1. private void breakBarrier() {
  2. generation.broken = true;
  3. count = parties;
  4. trip.signalAll();
  5. }

CyclicBarrier#breakBarrier方法非常简单,只做了3件事:

  • 标记CyclicBarrier被打破;
  • 重置CyclicBarrier的计数器;
  • 唤醒全部等待中的线程。

也就是说,一旦有个线程标记为中断状态,都会直接打破CyclicBarrier的屏障。
我们先跳过第2部分的唤醒逻辑,直接来看第3部分线程进入等待状态的逻辑。根据timed参数选择调用Condition不同的等待方法,随后是对异常的处理和线程中断状态的处理,同样是调用CyclicBarrier#breakBarrier,标记CyclicBarrier不可用。线程进入等待状态的逻辑并不复杂,本质上是通过AQS的Condition来实现的。
最后来看第2部分唤醒所有等待中线程的操作,根据计数器是否为0判断是否需要进行唤醒。如果需要唤醒,最后一个执行CyclicBarrier#await的线程执行barrierCommand(此时尚未执行任何线程唤醒的操作),做通过屏障前的处理操作,接着调用CyclicBarrier#nextGeneration方法:

  1. private void nextGeneration() {
  2. trip.signalAll();
  3. count = parties;
  4. generation = new Generation();
  5. }

CyclicBarrier#nextGeneration方法也做了3件事:

  • 唤醒所有Condition上等待的线程;
  • 重置CyclicBarrier的计数器;
  • 创建新的Generation对象。

很符合进入“下一代”的名字,先唤醒“上一代”所有等待中的线程,然后重置CyclicBarrier的计数器,最后更新CyclicBarrier的Generation对象,对CyclicBarrier进行重置工作,让CyclicBarrier进入下一个纪元。
到这里我们不难发现,CyclicBarrier自身只做了维护计数器和重置计数器的工作,而保证互斥性和线程的等待与唤醒则是依赖AQS家族的成员完成的:

  • ReentrantLock保证了同一时间只有一个线程可以执行CyclicBarrier#await,即同一时间只有一个线程可以维护计数器;
  • Condition为CyclicBarrier提供了条件等待队列,完成了线程的等待与唤醒的工作。

    CyclicBarrier#reset方法

    最后我们来看CyclicBarrier#reset方法:
    1. public void reset() {
    2. final ReentrantLock lock = this.lock;
    3. lock.lock();
    4. try {
    5. // 主动打破CyclicBarrier
    6. breakBarrier();
    7. // 使CyclicBarrier进入下一代
    8. nextGeneration();
    9. } finally {
    10. lock.unlock();
    11. }
    12. }
    CyclicBarrier#reset方法都是老面孔,先是CyclicBarrier#breakBarrier打破上一代CyclicBarrier,既然要重新开始就不要再“怀念”过去了;最后调用CyclicBarrier#nextGeneration开始新的时代。需要注意的是,这里加锁的目的是为了保证执行CyclicBarrier#reset时,没有任何线程正在执行CyclicBarrier#await方法。
    好了,到这里CyclicBarrier的核心内容我们就一起分析完了,剩下的方法就非常简单了,相信通过名字大家就可以了解它们的作用,并猜到它们的实现了。
    TipsCyclicBarrier#getNumberWaiting中加了锁,这是为什么?

    CountDownLatch和Cyclicbarrier有什么区别?

    最后的部分,我们来解答下开篇时的面试题,CountDownLatch和Cyclicbarrier有什么区别?
    第1点:CyclicBarrier可以重复使用,CountDownLatch不能重复使用
    无论是正常使用结束,还是调用CyclicBarrier#reset方法,Cyclicbarrier都可以重置内部的计数器
    第2点:Cyclicbarrier只阻塞调用CyclicBarrier#await方法的线程,而CountDownLatch可以阻塞任意一个或多个线程
    CountDownLatch将计数减1与阻塞拆分成了CountDownLatch#countDownCountDownLatch#await两个方法,而Cyclicbarrier只通过CyclicBarrier#await完成两步操作。如果在同一个线程中连续CountDownLatch#countDownCountDownLatch#await则实现了与CyclicBarrier#await方法相同的功能。

如果本文对你有帮助的话,还请多多点赞支持。如果文章中出现任何错误,还请批评指正。最后欢迎大家关注分享硬核Java技术的金融摸鱼侠王有志,我们下次再见!