【JUC知识】CyclicBarrier循环屏障源码解析(基于jdk11)

CyclicBarrier循环屏障源码解析(基于jdk11)

1.1 CyclicBarrier概述

public class CyclicBarrier extends Object

CyclicBarrier被称为循环屏障/同步屏障,它可以使一定数量的线程反复在"屏障"上汇集,当线程达到"屏障"位置时将调用await(),这个方法将阻塞该线程直到所有线程都到达屏障位置。如果足够数量的线程都到达屏障位置,那么屏障将打开,此时所有的线程都将被唤醒进而释放执行,而屏障将被重置以便下次使用。

通过它可以实现让一组线程互相等待共同到达某个状态之后再全部同时执行,叫做“循环”是因为当足够数量的等待线程都被释放以后,CyclicBarrier可以被重复使用。

1.2 CyclicBarrier原理

1.2.1 基本结构(jdk11)

通过UML类图可知,CyclicBarrier是使用了ReentrantLock和Condition来完成屏障效果,本质上底层还是基于AQS的,只不过更加高级。

关键属性:

  1. parties:parties是在创建CyclicBarrier的时候指定的值,后续不可更改,表示屏障点数,或者说表示需要多少线程到达屏障(调用await)后,所有线程才会打破屏障继续往下运行
  2. count:count则初始化为parties的值,每当有一个线程到达屏障调用await方法之后,count就就递减1;当count 为0 时,表示所需要的所有线程都到了屏障,此时屏障可以被打破

变量parties始终用来记录所需总线程个数,而当count 值变为0后,又会将parties 的值赋给count,从而进行复用。使用两个变量的原因就是为了实现CyclicBarrier 的可复用性。

barrierCommand是一个任务,当所需要的线程都到达屏障后执行的回调任务。一个Generation内部类表示屏障的实现,generation属性表示当前屏障。

这里的lock锁用于控制线程并发的,保证代线程安全,await、reset、isBroken、getNumberWaiting方法都需要获取锁。

1.2.2 await()方法

public int await(long timeout, TimeUnit unit)
        throws InterruptedException,
               BrokenBarrierException,
               TimeoutException {
        return dowait(true, unit.toNanos(timeout));
    }

这个await不是Condition的方法。调用CyclicBarrier 的await方法表示当前线程到达屏障点。如果当前线程不是将到达的最后一个线程,当前线程将一直等待。

满足下面条件之一将会被唤醒,可能还会抛出异常:

  1. 所需的所有线程都调用了await()方法(正常结束);
  2. 其他某个线程中断当前线程,则当前线程被唤醒并且清除当前线程的已中断状态,随后将打破当前屏障,并唤醒其他线程,最后抛出InterruptedException;
  3. 其他某个线程中断其他等待的线程,则当前线程被唤醒并抛出InterruptedException;
  4. 其他线程调用reset()方法打破屏障并重置屏障,则当前线程被唤醒并抛出BrokenBarrierException;
  5. 其他线程在等待当前屏障时超时,则当前线程被唤醒并抛出 BrokenBarrierException;
  6. 最后一个线程在执行回调任务过程中发生异常,则当前线程被唤醒并抛出 BrokenBarrierException。

如果当前线程是最后一个将要到达的线程,则当前线程不会等待,并且如果构造方法中提供了一个非空的回调任务,那么在允许其他线程继续运行之前(唤醒其他等待的线程之前),当前线程将运行该任务。如果在执行回调任务过程中发生异常,则该异常将传播到当前线程中,将 barrier 置于损坏状态,最后当前线程抛出该异常。

内部调用了dowait核心方法。

1.2.3 dowait方法

dowait是CyclicBarrier的核心方法,完成各种判断逻辑,比如等待、唤醒机制。

大概步骤如下:

  1. 首先就是获取lock锁,保证线程安全。

    final ReentrantLock lock = this.lock;
    lock.lock();
    
  2. 在一个try块中。获取当前屏障,使用局部变量g保存;

  3. 如果当前屏障被打破了,那么直接抛出BrokenBarrierException异常

  4. 如果当前线程被中断了,那么调用breakBarrier打破当前屏障,随后抛出InterruptedException异常。

     try {
                final Generation g = generation;
    
                if (g.broken)
                    throw new BrokenBarrierException();
    
                if (Thread.interrupted()) {
                    breakBarrier();
                    throw new InterruptedException();
                }
    ...
    
  5. count自减1,index记录到达的当前线程的索引,即自减1之后的count值;

  6. 如果index为0,那么表示当前线程是最后一个达到屏障的线程,所需要的所有线程都到达了屏障点:

    1. ranAction变量表示回调任务执行是否成功,初始化为false,表示执行失败;
    2. 开启一个try块:
      1. command变量记录回调任务;
      2. 如果command不为null,那么在当前线程(最后一个达到屏障的线程)中执行回调任务;
      3. 到这一步,表示command的执行没有抛出异常,那么ranAction设置为true;
      4. 调用nextGeneration重置屏障,并唤醒其他线程;
      5. 返回0,方法正常结束。
    3. 无论上面的有没有抛出异常(特指command任务的执行),都会执行finally代码块:
      1. 如果ranAction为false,表示command执行抛出了异常。那么调用breakBarrier打破当前屏障,并唤醒其他线程,随后抛出遇到的异常。

  1. 到这一步,表示index不为0,那么表示当前线程不是最后一个达到屏障的线程,可能需要等待。开启一个死循环:
    1. 开启一个开启一个try块:
      1. 如果是非超时等待,那么调用trip.await(),当前线程在trip条件变量上等待,直到被中断或者被唤醒。
      2. 否则,就是超时等待。如果超时时间大于0。那么调用trip.awaitNanos(),当前线程在trip条件变量上超时等待最多nanos纳秒,直到被中断或者被唤醒或者超时等待完毕,返回nanos,表示剩余超时等待时间。
    2. 在catch块中尝试捕获InterruptedException,即线程中断异常,如果捕获成功:
      1. 如果屏障g还是当前屏障,并且g没有被打破
        1. 那么当前线程调用breakBarrier打破当前屏障,并唤醒其他线程,随后抛出该异常。
      2. 否则,表示一种极端情况,即当前线程因为被中断而唤醒,但是由于cpu轮换,或者锁已被其他线程获取,还没有来得及打破屏障。此时最后一个线程就调用nextGeneration重置屏障成功,或者屏障被其他线程打破,或者屏障被其他线程reset。
        1. 设置当前线程的中断状态。这种情况如果是屏障正常打破,那么不需要抛出异常,算作等待完成,而如果是其他情况将会在下面判断并抛出异常!

  1. 到这一步,表示被唤醒或者超时时间到了,或者被中断但是其他线程更改了屏障设置的情况。如果屏障g被打破,那么抛出BrokenBarrierException异常。
  2. 如果屏障g不是当前屏障,说明最后一个线程已经到了,并且该屏障被打破并重置,返回index,正常结束。
  3. 如果是超时操作,并且等于超时时间小于等于0,那么说明是超时时间到了,并且该屏障还没有被打破。那么当前线程调用breakBarrier打破当前屏障,并唤醒其他线程,最后抛出TimeoutException异常。
  4. 到这里,说明屏障g既没有被打破也没有被替换,那么继续下一次循环,此时可能会继续等待。这种情况发生的概率很低,这种唤醒被称作“虚假唤醒”
  5. 最终需要在finally中释放lock锁。

1.2.3.1 breakBarrier打破屏障

在出现异常的时候调用的方法,且必须在获得锁之后才会调用。用于打破当前屏障,表明这个屏障已经失效了。主要做三件事:

  1. 打破当前屏障(broken设置为true);
  2. count重置为parties;
  3. 唤醒所有在trip条件变量上等待的线程。

1.2.3.2 nextGeneration重置屏障

在正常完成的时候调用的方法,且必须在获得锁之后才会调用。用于重置当前屏障,表明这个屏障已经使用完毕。主要做三件事:

  1. 唤醒所有在trip条件变量上等待的线程;
  2. count重置为parties;
  3. 重新初始化一个Generation对象,赋给generation,这就是下一个屏障。

可以看到,以前的屏障被第丢弃,但是并没有被打破(broken没有设置为true)。

1.2.4 await(timeout, unit)超时等待

public int await(long timeout,TimeUnit unit)

调用await方法表示当前线程到达屏障点。如果当前线程不是将到达的最后一个线程,当前线程将最多等待指定的超时时间。满足下面条件之一将会被唤醒,可能还会抛出异常:

与await()方法相似

1.2.5 reset重置屏障

将打破当前屏障并且重置新屏障。所有在屏障处等待的线程将会被唤醒并且抛出BrokenBarrierException。

实际上就是在获得锁之后连续调用breakBarrier和nextGeneration方法!

1.3 CyclicBarrier的总结

CyclicBarrier和之前学习CountDownLatch有些相似,但是又有区别:

  1. CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同。CountDownLatch一般用于某个或者某一批线程等待若干个其他线程执行完任务之后,它才执行;而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;
  2. 同一个线程中调用多次CountDownLatch的countDown方法,计数器就会减去多次;而同一线程中调用多次cyclicBarrier的await方法,还是只会算作一条线程到达当前屏障,因为调用一次await之后就会等待,而在所有线程都到达屏障之后,屏障开放并且重置,后续的await方法将算作在新屏障上的等待!
  3. CountDownLatch的计数器只能使用一次,而CyclicBarrier的屏障可以使用reset()方法重置,也会自动重置,所以CyclicBarrier能处理更为复杂的业务场景。
  4. CountDownLatch是使用原始的AQS框架实现的,而CyclicBarrier使用的则是更加高级的组件ReentrantLock和Condition,但是追根溯源,这两个组件也是依赖AQS实现的。
posted @ 2022-12-06 14:13  simonlee_java  阅读(31)  评论(0编辑  收藏  举报