05-Java多线程、并发工具类之CountDownLatch和CyclicBarrier

一、CountDownLatch

1.1 概念

  • 闭锁。CountDownLatch的作用是让一个或者多个线程等待一系列的指定操作完成后再开始执行。下面是其源码的英文注释,基本是就是这个意思:
  A synchronization aid that allows one or more threads to wait until  a set of operations being performed in other threads completes.

1.2 示例

  • 我们先看源码前面Doug Lea大师给的2个例子:

1.2.1 示例一

  • 这个示例中定义了2个信号,第一个开始信号是主线程完成指定工作之后,工作线程才开始工作;第二个结束信号是在工作线程完成之后主线程才能继续最后的执行。
class Driver {  
    void main() throws InterruptedException {
      CountDownLatch startSignal = new CountDownLatch(1); //开始信号
      CountDownLatch doneSignal = new CountDownLatch(N);  //结束信号
 
      for (int i = 0; i < N; ++i) // create and start threads
        new Thread(new Worker(startSignal, doneSignal)).start();
        doSomethingBefore();            // 主线程先完成指定工作
        startSignal.countDown();      // 主线程扣减之后,工作线程才开始工作
        doSomethingElse();            //主线程做其他的工作
        doneSignal.await();           // 主线程等待工作线程的完成之后才能进行后续的工作
        doSomethingElse();            //主线程等待工作线程完成之后,做清尾工作    
    }
  }
 
 class Worker implements Runnable {
    private final CountDownLatch startSignal;
    private final CountDownLatch doneSignal;
    Worker(CountDownLatch startSignal, CountDownLatch doneSignal) {
      this.startSignal = startSignal;
      this.doneSignal = doneSignal;
    }
    public void run() {
      try {
        startSignal.await();//工作线程开始之后,阻塞等待,直到主线程doSomethingBefore();完成之后才能继续
        doWork();
        doneSignal.countDown();//工作线程完成之后,扣减,所有工作线程扣减完之后,主线程才能开始清尾工作
      } catch (InterruptedException ex) {} // return;
    }
 
    void doWork() { ... }

1.2.2 示例二

  • 示例2是一个常见用法,将问题分为N部分,N个部分由工作线程执行完毕之后,调度的主线程才能继续执行。
class Driver2 {  
    void main() throws InterruptedException {
      CountDownLatch doneSignal = new CountDownLatch(N);
      Executor e = ...
 
      for (int i = 0; i < N; ++i) {
            //创建线程执行任务
            e.execute(new WorkerRunnable(doneSignal, i));
        }
        doneSignal.await();// 主线程等待
        doSomethingElse(); //N个工作线程执行完毕之后,主线程才能继续执行
    }
  }
 
  class WorkerRunnable implements Runnable {
    private final CountDownLatch doneSignal;
    private final int i;
    WorkerRunnable(CountDownLatch doneSignal, int i) {
      this.doneSignal = doneSignal;
      this.i = i;
    }
    public void run() {
      try {
        doWork(i);
        doneSignal.countDown(); //工作线程扣减
      } catch (InterruptedException ex) {} // return;
    }
 
    void doWork() { ... }
 } 

1.2.3 用法小结

  • 我们看到CountDownLatch的用法是比较简单的,初始化的时候指定一个扣减数量,然后需要等待的线程就调用await方法,线程就会一直阻塞在这里,直到对应的扣减的被扣完,而这个扣减的动作往往是在另一个线程做的,这样就可以起到控制不同的线程执行顺序的目的,也印证了前面描述CountDownLatch的作用:让一个或者多个线程等待一系列的指定操作完成后再开始执行。注意等待的线程可以不止一个,我们前面的示例一种,工作线程等待主线程的第一个信号就是多个工作线程的等待。

1.3 源码解析

  • CountDownLatch的源码还是比较好阅读的,总量很少,包括注释在内才300余行,出去大量注释和一些类似于toString的方法,基本上量很少,比较适合我们了解并发根据的原理和思想,不过这一切都是基于AQS来实现的(AbstractQueuedSynchronizer),站在巨人的肩膀上,顶层的工具实现才会显得简洁。如下我们看到的源码方法很少,除了内部类Sync只有几个简单的方法,我们一一解读

[外链图片转存失败(img-7ySfyVm2-1565592675586)(https://note.youdao.com/yws/api/personal/file/0463F3106B184DA3B33ED23856FB1884?method=download&shareKey=db273de2b45028b93f93567b30962128)]

1.3.1 构造方法

  • 构造方法简单,传入一个扣除点数量,除了参数校验,主要目的是初始化sync,sync是内部类Sync的一个实例,Sync类我们在最后解析。
public CountDownLatch(int count) {
        //扣除点数量不能<=0
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }

1.3.2 await

  • 我们之前的入门示例分析了await的作用就是阻塞在那里,等其他地方扣减,扣减完毕之后就可以执行了。
   //await方法会调用AQS的acquireSharedInterruptibly方法
    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }
 

    AbstractQueuedSynchronizer#acquireSharedInterruptibly
    //这个方法是在AQS中实现的,是能够响应中断的获取共享状态的方法,其实就是获取state的值,
    //如果state为0,就放行线程,如果大于0,线程就需要进入队列
    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
            //这个tryAcquireShared方法就是Sync中重写的,如果state为0就会返回1,否则返回-1
            //如果state是0->tryAcquireShared返回1->acquireSharedInterruptibly正常执行完毕->线程就可以放行不需要等待了
            //如果state>0->tryAcquireShared返回-1->线程进入队列等待
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }
    
    protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
    }

1.3.3 支持超时的await

  • await有另一个重载,可以支持超时模式。支持超时的话线程就不会傻傻的一直等下去了。await(long timeout, TimeUnit unit)
    public boolean await(long timeout, TimeUnit unit)
        throws InterruptedException {
        //调用AQS的tryAcquireSharedNanos方法
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }
    
   //AbstractQueuedSynchronizer#tryAcquireSharedNanos
    public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
            //逻辑和await类似,先判断能不能放行,如果不能就进入队列等待,doAcquireSharedNanos里面的超时逻辑也是基于tryAcquireShared来实现的
        return tryAcquireShared(arg) >= 0 ||
            doAcquireSharedNanos(arg, nanosTimeout);
    }
 

1.3.4 countDown

  • 扣减方法。每次扣减一个扣除点,对应内部的state减一
    public void countDown() {
        //会调用AQS的releaseShared方法
        sync.releaseShared(1);
    }
    
    //AbstractQueuedSynchronizer#releaseShared
    public final boolean releaseShared(int arg) {
        //先调用tryReleaseShared方法将state减一,
        //如果扣减后为0就返回true,此时会进入判断内部做一些清理工作,
        //如果扣减后不为0,那么就直接返回false。这里的返回值在countDown方法里面并不关心
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

1.3.5 getCount

  • 返回当前剩余的扣除点,比如初始化为5,执行一次countDown之后,返回4,最后返回0的时候被阻塞的线程才会继续执行。
    public long getCount() {
        return sync.getCount();
    }

1.3.6 Sync内部类

  • 前面我们看到的几个方法都是基于Sync这个内部类来实现的,我们解读一下。
/**
     * Synchronization control For CountDownLatch.
     * Uses AQS state to represent count.
     * 同步控制CountDownLatch,使用AQS的state来表示count扣减数
     */
    private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;
        
        //构造方法,给state状态变量赋值
        Sync(int count) {
            setState(count);
        }
        //返回state的值
        int getCount() {
            return getState();
        }
         
         
        /**
        *尝试获取共享状态变量,如果state是0就返回1,反之大于0就返回-1
        *是await实现的基础
        */
        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }
        
        /**
        * 该方法是countDown方法实现的基础,
        * 方法内部通过CAS来将state减一
        * 返回false表示state数在被扣减后还大于0,
        * 返回true表示state为0已经没有扣减数了
        */
        protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
    }

1.4 CountDownLatch原理小结

  • 通过一个继承自AQS的类Sync来实现线程之间的同步,初始化AQS中的state来代表我们的扣除点。
  • 调用await其实就是获取到state并判断他是否为0,如果是0那么线程就放行,反之state大于0的话,线程就进入队列等待。
  • 调用countDown,实际上是将state减一,减一的时候使用了CAS保证线程安全,如果减一之后state是0,那么会做一部分清理相关的工作。
  • 至于线程同步对列的维护,我们不需要关心,这部分在AQS中实现的,回到AQS,我们可以看到它实现了一套固定的流程,屏蔽了大量细节,我们只需要实现state状态的获取(实际上是读)和释放(可以理解为写)逻辑即可实现不同场景下的并发应用工具。

二、CyclicBarrier

2.1 概念

  • CyclicBarrier是并发编程的工具类,翻译为栅栏。当一组线程需要全部到达某一个屏障点之后,后续的操作才可以继续执行,就可以使用CyclicBarrier,比如我们有四个数据采集线程和一个数据分析线程,我们需要在四个采集线程完成采集之后,再执行数据分析线程,可以借助CyclicBarrier来实现。
  • 按照源码的注释,我们看对这个工具类的描述:CyclicBarrier是同步工具,可以让一组线程相互等待到达一个共同的栅栏点。当程序涉及到一组固定数量的线程需要相互等待时,CyclicBarrier是非常有用的。之所以加上cyclic的前缀,是因为CyclicBarrier在现场释放之后还是可以重用的。
synchronization aid that allows a set of threads to all wait for
 * each other to reach a common barrier point.  CyclicBarriers are
 * useful in programs involving a fixed sized party of threads that
 * must occasionally wait for each other. The barrier is called
 * <em>cyclic</em> because it can be re-used after the waiting threads
 are released.
  • CyclicBarrier还支持传入一个Runnable类型的barrierAction参数,在一组线程最后一个线程到达栅栏点之后,并且在所有线程执行完之前,
    该barrierAction线程就会执行。当使用更新共享状态的方式来控制这一组线程的时候, barrierAction很有用,先看源码给的示例。

2.2 示例

2.2.1 示例一

  • 该示例的场景是:每一个工作线程Worker用来处理数组的一行,然后等到所有的行都处理完毕,之后barrierAction线程会执行合并结果的操作。
    +如果合并线程的操作确定结果已经找到,那么就可以控制done()方法返回true,这样每一个Worker线程就会退出循环终止线程了。
  class Solver {
    final int N;
    final float[][] data;
    final CyclicBarrier barrier;
 
    //每个工作线程用来处理数组的每一行,
    class Worker implements Runnable {
      int myRow;
      Worker(int row) { myRow = row; }
      public void run() {
       //1.如果没有结束就处理,因为有可能某一个线程还没有处理,就已经找到结果不需要处理了,因此这里先判断一下
        while (!done()) {
          processRow(myRow);
           //2.处理完之后,线程需要等待,等其他并行处理某一行的线程一起处理完 
          try {
            barrier.await();
          } catch (InterruptedException ex) {
            return;
          } catch (BrokenBarrierException ex) {
            return;
          }
        }
      }
    } 
 
    public Solver(float[][] matrix) {
      data = matrix;
      N = matrix.length;
      //定义一个barrierAction,这个线程是在所有的Worker都到达栅栏之后,才会执行mergeRows合并结果的操作
      Runnable barrierAction = new Runnable() { public void run() { mergeRows(...); }};
 
      barrier = new CyclicBarrier(N, barrierAction);
 
      List<Thread> threads = new ArrayList<Thread>(N);
      for (int i = 0; i < N; i++) {
        Thread thread = new Thread(new Worker(i));
        threads.add(thread);
        thread.start();
      }
 
      //等待工作线程
      for (Thread thread : threads)
        thread.join();
    }
  }

2.2.1 示例一代码

  • 下面我们使用代码将示例一实现,大概的场景是这样的:我们有一个10*10000的数组,我们将每一行的前面9999个元素都初始化为一个小于100的随机数,
    每一行的最后一个位置用来保存一个临时结果。我们处理行的逻辑是这样的,将每一行的前N个元素求和并将结果放到每一行的最后一个位置,比如第一次
    就是将每一行的首元素放到最后,第二次就是将每一行的前二个元素之和放到最后,第三次就是将每一行前三个元素之和放到最后,依次类推,显然随着次
    数的增加我们每一行最后保存的那个数字会越来越大。10个工作线程Worker处理完一次之和,就会等待,然后由合并线程MergeRows来将10行得到的10个结果
    加起来,如果加起来大于我们设置的阈值,我们就不再处理了,此时理解为我们找到结果了,如果加起来还不够阈值我们就再往后处理,直到总和大于阈值。
  • 源码

  • 输出:从下面的打印我们看出,因为我们随机数的上限是100,所以均值大概是50,因此每一次10个数求和大概是500,所以多次测试发现都需要累加到
    第四次才能大于阈值2000;我们看到每一次10个工作线程处理完之后就会阻塞,让合并线程处理,为了看到效果我将合并线程sleep了3秒钟模拟合并的过程,
    该过程中工作线程并不会处理,我们可以将工作线程和barrierAction线程之间理解为串行的关系,同时我们看到每一次执行barrierAction都是一个不同的
    线程,这里可能我们会认为是我们初始化好的那个线程,但是并不是,具体细节可能需要探究源码才能发现。从这个例子我们也印证了前面提到的CyclicBarrier
    是可以循环使用的,CountDownLatch通常只会用一次,扣减到0就放行阻塞的线程,并没有接口让我们增加扣除点, 最后我们会比较CyclicBarrier和CountDownLatch的区别,会提到这一点。
初始化data完毕...
线程:Thread-0处理第0行第0列...
线程:Thread-1处理第1行第0列...
线程:Thread-2处理第2行第0列...
线程:Thread-3处理第3行第0列...
线程:Thread-4处理第4行第0列...
线程:Thread-5处理第5行第0列...
线程:Thread-6处理第6行第0列...
线程:Thread-7处理第7行第0列...
线程:Thread-8处理第8行第0列...
线程:Thread-9处理第9行第0列...
MergeRows 开始Merge...Thread-9
MergeRows 完毕耗费3000ms ,Thread-9
MergeRows 失败 ,结果是: 455.0
线程:Thread-9处理第9行第1列...
线程:Thread-0处理第0行第1列...
线程:Thread-2处理第2行第1列...
线程:Thread-1处理第1行第1列...
线程:Thread-4处理第4行第1列...
线程:Thread-5处理第5行第1列...
线程:Thread-6处理第6行第1列...
线程:Thread-7处理第7行第1列...
线程:Thread-8处理第8行第1列...
线程:Thread-3处理第3行第1列...
MergeRows 开始Merge...Thread-3
MergeRows 完毕耗费3000ms ,Thread-3
MergeRows 失败 ,结果是: 837.0
线程:Thread-3处理第3行第2列...
线程:Thread-9处理第9行第2列...
线程:Thread-0处理第0行第2列...
线程:Thread-1处理第1行第2列...
线程:Thread-4处理第4行第2列...
线程:Thread-6处理第6行第2列...
线程:Thread-8处理第8行第2列...
线程:Thread-2处理第2行第2列...
线程:Thread-7处理第7行第2列...
线程:Thread-5处理第5行第2列...
MergeRows 开始Merge...Thread-5
MergeRows 完毕耗费3000ms ,Thread-5
MergeRows 失败 ,结果是: 1244.0
线程:Thread-5处理第5行第3列...
线程:Thread-3处理第3行第3列...
线程:Thread-9处理第9行第3列...
线程:Thread-0处理第0行第3列...
线程:Thread-4处理第4行第3列...
线程:Thread-1处理第1行第3列...
线程:Thread-8处理第8行第3列...
线程:Thread-2处理第2行第3列...
线程:Thread-6处理第6行第3列...
线程:Thread-7处理第7行第3列...
MergeRows 开始Merge...Thread-7
MergeRows 完毕耗费3000ms ,Thread-7
MergeRows 失败 ,结果是: 1815.0
线程:Thread-7处理第7行第4列...
线程:Thread-5处理第5行第4列...
线程:Thread-9处理第9行第4列...
线程:Thread-8处理第8行第4列...
线程:Thread-3处理第3行第4列...
线程:Thread-6处理第6行第4列...
线程:Thread-2处理第2行第4列...
线程:Thread-1处理第1行第4列...
线程:Thread-0处理第0行第4列...
线程:Thread-4处理第4行第4列...
MergeRows 开始Merge...Thread-4
MergeRows 完毕耗费3000ms ,Thread-4
MergeRows 成功 ,结果是: 2188.0
  • 关于barrierAction
  • 在上面的实现中,我们的barrierAction传进去的是一个合并结果的线程,这个线程和工作线程是不一样的,并且合并线程工作的时候,其余的工作线程是阻塞的。
    如果有一些特殊情况满足下面2点的话,我们可以简化;
    A:我们的工作线程就可以处理合并的结果,此时不需要另外的合并线程,
    B:合并结果的过程中,不需要所有线程在栅栏处阻塞,
    这样的话,我们可以使用最后一个到达栅栏点的线程来处理合并结果,此时其余的线程会继续执行,我们可以通过一个方法来判断线程是第几个到达栅栏点的,
    根据barrier.await() 的返回值来判断,如果有5个线程,那么第一个到达栅栏的线程返回值是4,最后一个线程的返回值是0,过程是依次递减的。模式如下:
if (barrier.await() == 0) {
     // 合并过程逻辑执行...
   }

2.3 源码解析

  • 关于CyclicBarrier的同步模式,CyclicBarrier在同步模式上使用all-or-none的模式,要么全部,要么没有。意思是要么全部线程正常到栅栏,然后再继续运行,
    如果有一个线程因为某种原因不能到栅栏点,其他线程也会抛出异常。具体我们看await方法;
  • 关于内存一致性:线程中await()方法之前的行为 happens before 于 barrierAction中的行为,barrierAction中的行为 happens before 于线程中
    await()返回之后的行为,如下:
AAA
await()
BBB
按照优先级理解为:AAA > barrierAction > BBB

2.3.1 await方法

  • 阻塞等待方法。阻塞线程直到所有的线程都调用了CyclicBarrier的await方法。
  • 如果当前线程不是最后一个到达栅栏的线程,那么它就不会被调度,直到下面的情况发生:
1.最后一个线程到达栅栏
2.该线程被中断(即线程的interrupt方法被其他线程调用)
3.其他等待栅栏的线程被中断
4.其他等待栅栏的线程超时释放
5.栅栏的reset()方法被调用
  • 线程在await期间如果被中断,线程会抛出InterruptedException。此时如果有其他线程A阻塞在栅栏,则线程A会抛出BrokenBarrierException,
    并且栅栏会被破坏 (isBroken返回true)
  • 如果线程在等待期间,栅栏被reset,,或者被破坏(isBroken返回true),或者在破坏状态下调用await,都会抛出BrokenBarrierException
  • 如果当前线程是最后一个到达栅栏的线程,并且没有指定barrierAction,那么在其他线程执行之前,当前线程会执行Action,如果过程中发生了异常,那么当前线程会
    抛出异常并且栅栏会被破坏。
    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));
    }
  • 我们看到2个await方法底层都是调用dowait方法,猜到dowait方法应该是通过boolean参数来支持2种模式

2.3.2 CyclicBarrier#dowait

  • await的内部实现
private int dowait(boolean timed, long nanos) throws InterruptedException, BrokenBarrierException,TimeoutException {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            //Generation是定义的一个内部类,这个类极其简单,就只包含一个boolean类型的属性代表当前栅栏是否被破坏,false表示没有被破坏,true表示被破坏了。
            //我们在前面提到过栅栏是可以重复使用的,我们的示例1的实现也佐证了这一点,每一次使用栅栏,都相对应于一个Generation实例,后面我们看源码对Generation
            //的源码注释进行解读。这个Generation是不需要我们通过构造方法实例化的,我们每实例化一次CyclicBarrier,里面都有一个Generation
            //对于CyclicBarrier,我们看方法图可以知道,可以操作的api主要就是await方法,其余的几个方法基本上是读方法,就是用来获取关于CyclicBarrier
            //内部信息的,因此每一次await
            final Generation g = generation;
            
            //2.如果栅栏被破坏旧抛出异常
            if (g.broken)
                throw new BrokenBarrierException();

            //3.如果线程被中断,就破坏栅栏,并抛出中断异常
            if (Thread.interrupted()) {
                breakBarrier();
                throw new InterruptedException();
            }

            //count代表当前还没有到达栅栏的线程,既然当前线程调用了await,说明它已经到栅栏了,因此count减一
            int index = --count;
            //如果减一之后为0,说明当前线程是最后一个了,
            if (index == 0) {  // tripped
                boolean ranAction = false;
                try {
                     //如果构造方法传入了barrierAction,就让barrierAction执行,(barrierCommand就是barrierAction)
                    final Runnable command = barrierCommand;
                    if (command != null)
                        command.run();
                    ranAction = true;
                    nextGeneration();
                    return 0;
                } finally {
                    //看上面的逻辑可知,如果构造方法没有传入barrierAction,说明没有barrierAction需要执行,那么就需要放行全部的线程,此时会进入下面这个
                    //if逻辑,这时候就会打破栅栏,在breakBarrier方法里面会将generation.broken = true表示栅栏已经破坏,同时初始化count为初始值,并且唤
                    //醒全部的其他线程,(其实这里比较形象,就好比几个人约定达到栅栏后再一起进行后续的工作,现在最后一个人到了,他就打破栅栏并唤醒其他伙伴)
                    if (!ranAction)
                        breakBarrier();
                }
            }

            // loop until tripped, broken, interrupted, or timed out
            //如果index不是0,很简单,说明自己并不是最后一个线程,此时应该阻塞等待后面的线程,由最后面的线程来负责后面的
            //指挥(要么调用barrierAction,要么唤醒大家一起执行后面的步骤)
            for (;;) {
                //在于一个for循环里面自旋,
                try {
                    if (!timed)
                        //如果不是超时模式,那么就直接在Condition上await即可,此时会释放锁,然后后面的伙伴可能会进来执行这个代码块
                        //这里需要掌握的一点是,调用await方法会自动释放锁,然后再次往下执行就会获得锁,这个过程是JVM保证的,非常基础且重要的知识点
                        //在前面使用Lock对象加锁之后,调用Lock生成的condition对象的await是会将锁释放的,
                        //这里再补充一点,如果直接调用lock的wait是会抛出IllegalMonitorStateException,因为我们的锁对象根本不是lock这个对象
                        trip.await();
                    else if (nanos > 0L)
                        //如果是超时模式,那么就在Condition上执行支持超时的await
                        nanos = trip.awaitNanos(nanos);
                } catch (InterruptedException ie) {
                    //阻塞的线程被打断,我们前面有写过,需要打破栅栏,而且自己会抛出异常
                    if (g == generation && ! g.broken) {
                        //如果栅栏的标志是false(false表示没有打破),那么就打破栅栏,抛出异常
                        breakBarrier();
                        throw ie;
                    } else {
                        //走到这逻辑说明g的broken状态是true,说明栅栏已经打破了,就不需要我们关心了,中断自己即可(管好自己就行啦)。
                        //因为不自己中断自己的话,自己还会继续运行的,捕获异常后中断标志位会被复位(不了解这个的需要补补啦)
                        // 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();
                
                //这个条件说明generation被更新了,前面一点点在执行完barrierAction会更新generation,这里再次验证了之前的说法,一次栅栏的使用对
                //应一个generation对象,运行到此处,其实说明在自己等待的过程中(自己不是最后的线程),最后的线程跑到了栅栏点,并且更新了generation
                //对象,就好比最后一个线程宣布,这一次我们可以继续执行了,我们需要一起跑到下一个栅栏(不管有没有下一个栅栏,反之这次结束了),
                //因此本线程也不需要再傻傻等待了,直接返回,返回值是代表当前线程跑到栅栏的顺序(如果是5个线程,那么第一个到的线程返回4,第二个返回3,,最后一个返回0)
                if (g != generation)
                    return index;
                
                //如果执行到这里,说明最后一个线程还没到栅栏(它到的话,要么会更新generation,要么打破栅栏,不管哪一种,在前面2个判断就会返回了)
                //那么就检查是否超时,超时就打破栅栏,抛异常
                if (timed && nanos <= 0L) {
                    breakBarrier();
                    throw new TimeoutException();
                }
            }
        } finally {
            lock.unlock();
        }
    }

2.3.3 其他Api

2.3.3.1 CyclicBarrier#getNumberWaiting
  • 方法返回在栅栏等待的线程数,parties代表总的线程数,count代表还未到栅栏的线程数(也是每一个线程到达栅栏的返回值,比如5个线程,第一个线程到栅栏返回的就是4)
public int getNumberWaiting() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return parties - count;
        } finally {
            lock.unlock();
        }
    }
2.3.3.2 CyclicBarrier#reset
  • 破坏栅栏,并且更新内部的Generation对象
public void reset() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            breakBarrier();   // break the current generation
            nextGeneration(); // start a new generation
        } finally {
            lock.unlock();
        }
    }
  • isBroken方法:判断当前栅栏是否被破坏
  • getParties:获取线程数量

三、CountDownLatch和CyclicBarrier对比

  • 初学者容易将二者弄混淆,我们在这里对CountDownLatch和CyclicBarrier做下对比
对比项CountDownLatchCyclicBarrier
原理基于AQS来实现内部使用Condition+CAS来实现(dowait里面的for死循环可以理解为CAS)
实现细节使用AQS的state来代表扣除点,await和countDown对应state的判断和原子自减使用锁机制,由最后到达线程来确定整组线程的放行,被阻塞的线程自旋等待
特点不可重复使用,扣减不可逆可循环使用,每次使用通过内部的Generation对象来代表当前该次使用期间栅栏的状态(最后达到栅栏的线程会负责更新Generation对象)
应用场景一个或者一组线程的执行由其他线程来控制,比如发令枪,让所有线程阻塞在一个地方,由外部线程coutDown放行一组线程的放行由该组线程内部决定,比如多个线程协作分阶段完成任务,每个阶段可以做同步等待

四、Tips

  • cyclicBarrier初始化的时候会指定到达屏障点的线程数,如果实际线程数小于该数目,就会一直在屏障点阻塞下去。
  • await方法也可以指定超时时间,如果第一条中线程数目少于初始化指定的数目或者其他原因,await不会一直阻塞,超时后会破坏栅栏

五、链接

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值