大厂之路一由浅入深、并行基础、源码分析一 “J.U.C.L”之倒计数器CountDownLatch、循环栅栏CyclicBarrier源码级分析(基于AQS、共享锁)

AQS 定义了两种资源共享方式

倒计数器CountDownLatch

  • CountDownLatch是一个非常实用的多线程控制工具类, “CountDown”在英文中意为 “门闩”,也就是说把门锁起来,不让里面的线程跑出来。
    • 我们可以想象一下火箭发射,只有万事俱备,都调试好,才能发射。CountDownLatch可以使最后的点火线程等待所有检查线程全部完工后再执行
  • 我们用术语进一步描述:
    • CountDownLatch主要提供的机制是当多个(具体数量等于初始化CountDownLatch时count参数的值)线程都达到了预期状态 或完成预期工作时才可以触发事件,其他线程可以等待这个事件来触发自己的后续工作。
    • 到达自己预期状态的线程会调用CountDownLatchcountDown方法,等待的线程会调用CountDownLatch的 await方法
    • 值得注意的是,CountDownLatch是可以唤醒多个等待的线程的
    • 如果CountDownLatch初始化的count值为1,那么这就退化为一个单一事件了,也就是由一个线程来通知其他线程,效果等同于对象的wait和notifyAll, count值大于1是常用的方式,目的是为了让多个线程到达各自的预期状态,变为一个事件进行通知,线程则继续自己的行为。

倒计数器CountDownLatch的结构

  • 分析前我们先看一下它的结构图:
    dsdaasdasdsadasdasd在这里插入图片描述
  • 通过结构图我们发现:CountDownLatch类只提供了一个构造器,并且 CountDownLatch类有一个内部类Sync,Sync继承于AQS,并没有其他得复杂结构,我们接着往下看:
  • CountDownLatch类只提供了一个构造器:构造器调用了CountDownLatch中的内部类Sync的 setState(count)方法,我们发现这个方法实现在AQS基础组件中,并且count值就是同步状态state的值,如图:
    public CountDownLatch(int count) {    //参数count为计数值,当<0时,抛出异常🧡
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }
  🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 
         Sync(int count) {
         setState(count);
     }
  🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 
     //AQS中的方法     count值就是同步状态state的值🧡
     protected final void setState(int newState) {
      state = newState;
  }

倒计数器CountDownLatch的方法

    // 使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断。🧡
    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }
 	// 使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断或超出了指定的等待时间。
    public boolean await(long timeout, TimeUnit unit)
        throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }
	// 递减锁存器的计数,如果计数到达零,则释放所有等待的线程。
    public void countDown() {
        sync.releaseShared(1);
    }
	// 返回当前计数。
    public long getCount() {
        return sync.getCount();
    }
	// 返回标识此锁存器及其状态的字符串。
    public String toString() {
        return super.toString() + "[Count = " + sync.getCount() + "]";
    }

倒计数器CountDownLatch的使用

  • 模拟火箭发射程序:
package com.wwj.lockdemos;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CountDownLatchDemo implements  Runnable{
    static  final CountDownLatch end = new CountDownLatch(5);
    static  final CountDownLatchDemo demo = new CountDownLatchDemo();
    @Override
    public void run() {
        try{
            //模拟检查任务
           	System.out.println(end.toString());
          	System.out.println(end.getCount());
            Thread.sleep((int)Math.random()*1000);
            System.out.print("检查完成 ");
            end.countDown();   //完成任务才进行countDown()🧡
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws  InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for(int i=0 ; i<5 ; i++){
            executorService.submit(demo);
        }
        //都要进行等待
        end.await();
        System.out.println("Fire");

        executorService.shutdown();
    }
}

//结果:java.util.concurrent.CountDownLatch@bdcdf9e[Count = 5]
5
java.util.concurrent.CountDownLatch@bdcdf9e[Count = 5]
5
java.util.concurrent.CountDownLatch@bdcdf9e[Count = 5]
java.util.concurrent.CountDownLatch@bdcdf9e[Count = 5]
5
5
检查完成 检查完成 检查完成 检查完成 java.util.concurrent.CountDownLatch@bdcdf9e[Count = 1]
1
检查完成 Fire
//当我们把CountDownLatch的值设置成6、结果:检查完成 检查完成 检查完成 检查完成 检查完成
  • 程序分析:当生成一个CountDownLatch实例,计数量为5,这表示CountDownLatch上的线程(点火线程)需要这等待这5个线程完成任务(检查线程)后才能继续执行。每执行完一次检查,就通过countDown()方法来通知CountDownLatch,让其倒计数器减1,其实就是同步状态state减1。如图:
  • 在这里插入图片描述
  • 通过结果我们发现:
    • 当我们把CountDownLatch的值设置成6,最后结果不会有Fire,程序也不会停止运行,因为它一直等待第6个线程去执行;
    • 通过getCount()方法我们也可以看出程序真正实现了多线程同时处理临界区域,因为返回了四个Count==5;
  • 接下来我们进一步分析其原理。

倒计时器CountDownLatch 实现原理

  • 我们重点分析两个方法await()tryAcquireShared(int acquires)方法(因为最核心的程序前面的文章已经分析过,不清楚的可以去看一下):

重点看await()的源码:

🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡
public void await() throws InterruptedException { 
        //尝试获取资源,等待的时候支持线程响应💚
        sync.acquireSharedInterruptibly(1);   //该函数实际上是调用的AQS的acquireSharedInterruptibly(1);💚
}
🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡
public final void acquireSharedInterruptibly(long arg) throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0)   //CountDownLatch重写的方法tryAcquireShared(arg)判断是否能获得同步状态💚
        doAcquireSharedInterruptibly(arg);
}
🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡
protected int tryAcquireShared(int acquires) {    这个方法可以去和Semaphore和ReentrantLock的分别对比🖤🖤
//判断state是否为0了,如果为0就返回1,表示不用等待了,可以打开门闩了!!。返回-1的话线程就要挂起等待state为0💚
    return (getState() == 0) ? 1 : -1;
}
🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡
private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
   //创建共享模式的结点Node.SHARED,并加入同步队列
   final Node node = addWaiter(Node.SHARED);     //只有await()方法调用了,说明只有执行await()方法 的线程进入等待队列
     boolean failed = true;
     try {
         //进入自旋操作🧡
         for (;;) {
             final Node p = node.predecessor();
             //判断前驱结点是否为head🧡
             if (p == head) {
                 //尝试获取同步状态🧡
                 int r = tryAcquireShared(arg);
                 //如果r>0 说明获取同步状态成功🧡  ,这里只有等于同步状态等于0,即r=0的时候才能执行下面的操作
                 if (r >= 0) {
                     //将当前线程结点设置为头结点并传播(Semaphore中有讲到)🧡               
                     setHeadAndPropagate(node, r);
                     p.next = null; //帮助GC回收
                     failed = false;
                     return;
                 }
             }
 //调整同步队列中node结点的状态并判断是否应该被挂并,并判断是否需要被中断,如果中断直接抛出异常,当前结点请求也就结束🧡
             if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                 throw new InterruptedException();
         }
     } finally {
         if (failed)
             //结束该结点线程的请求
             cancelAcquire(node);
     }
   }
  • acquireSharedInterruptibly()的作用是获取共享锁:
    • 如果当前线程是中断状态,则抛出异常InterruptedException
    • 否则,调用tryAcquireShared(arg)尝试获取共享锁;
      • 尝试成功则返回,否则就调用doAcquireSharedInterruptibly(),该方法会使当前线程一直等待,直到当前线程获取到共享锁(或被中断)才返回。

再看cutdown()方法源码:

🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡
   public void countDown() {
        //该函数实际上调用releaseShared(1)释放共享锁 ,releaseShared()在AQS中实现
        sync.releaseShared(1);    
}
🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡
  public final boolean releaseShared(int arg) {
   //当tryReleaseShared返回true,才释放(这是JDK15,和JDK8略有不同)
   //只有state==0,时才通知同步队列中的下一个结点
      if (tryReleaseShared(arg)) {     💛💛💛💛💛  //一直纠结的问题终于搞明白了:!!!1
          signalNext(head);				//不说什么问题了,就说一个细节:
          return true;				//不是每个线程都进入同步队列,所以信号量的释放不是持有锁的线程释放的,
          //因							因为压根就没有持有锁 ,同步队列中存放的是执行await()方法的线程,
        							    //一般这个线程为等待的线程;💛💛💛💛💛
      }									
      return false;
  }
🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡
   protected boolean tryReleaseShared(int releases) {
       // Decrement count; signal when transition to zero   减少至0的时候通知signalNext(head);
        // 自旋+CAS : tryReleaseShared()的作用是释放共享锁,将“锁计数器”的值-1,这是CountDownLatch实现的
       for (;;) {
           int c = getState(); // 获取“锁计数器”的状态
           if (c == 0)
               return false;
           int nextc = c - 1;
           if (compareAndSetState(c, nextc))
               return nextc == 0;   //只有state==0,时才通知同步队列中的下一个结点
       }
   }
🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡🧡🧡 🧡🧡
   private static void signalNext(Node h) {
       Node s;  
  //当头节点不为空,且下一个结点也不为空,所以将其结点设置为waiting,在JDK15中waiting就是可以唤醒状态,
  //然后将这个结点unpark(),因为在同步队列中的头节点的后驱结点都是挂起park()状态
       if (h != null && (s = h.next) != null && s.status != 0) {
           s.getAndUnsetStatus(WAITING);
           LockSupport.unpark(s.waiter);
       }
   }
  • 用法总结
    • CountDownLatchcountDown()用法是当一个线程执行完工作后,通过countDown()来使同步状态state-1,并且只有同步状态为0时才会通知同步队列中头节点的下一个结点,改变其状态然后unpark()
    • 通过await()方法,只有同步状态为0时,才能加入到队列中,后续的操作就和Semaphore都一样了,通过await()方法我们发现它执行完,并不会重置同步状态state,那这时候就不能继续执行下去,也可以说CountDownLatch是一次性的,那么有没有它的改善呢?当然,就是接下来要讲的循环栅栏CyclicBarrier,期待!
  • 总结:
    • CountDownLatch是通过“共享锁”实现的;
    • 在创建CountDownLatch中时,会传递一个int类型参数count,该参数是“锁计数器”的初始状态,表示该“共享锁”的同步状态,即最多能被count个线程同时获取。
    • 当某线程调用该CountDownLatch对象的await()方法时,该线程会等待“共享锁”可用时,才能获取“共享锁”进而继续运行;
    • 而“共享锁”可用的条件,就是“锁计数器”的值为0!而“锁计数器”的初始值为count,每当一个线程调用该CountDownLatch对象的countDown()方法时,才将“锁计数器”-1;通过这种方式,必须有count个线程调用countDown()之后,“锁计数器”才为0,而前面提到的等待线程才能继续运行!

循环栅栏CyclicBarrier

  • CyclicBarrier是另外一种多线程并发工控制工具。其实和CountDownLatch非常类似,它也可以实现线程间的计数等待,但是它比CountDownLatch更加复杂,但是如果了解ReentrantLock和Condition的话,其实比CountDownLatch更简单。
  • CyclicBarrier是一个同步辅助类,允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。因为该 barrier 在释放等待线程后可以重用,所以也称它为循环的栅栏。
  • CyclicBarrierCountDownLatch的对比:
    • CountDownLatch 基于 AQS 的共享模式的使用,而 CyclicBarrier 基于 Condition 来实现的
    • CountDownLatch的作用:允许1或N个线程等待其他线程完成执行而CyclicBarrier则是允许N个线程相互等待
    • CountDownLatch的计数器无法被重置;CyclicBarrier的计数器可以被重置后使用

循环栅栏CyclicBarrier的结构

  • 分析CyclicBarrier前我们先看一下它的结构图:
    在这里插入图片描述
  • 从结构图可以看出:它的逻辑结构很简单,CyclicBarrier是包含了"ReentrantLock对象lock"和"Condition对象trip",它是通过独占锁实现的:
  • CyclicBarrier类的内部有一个计数器(没有标注出来),每个线程在到达屏障点的时候都会调用await()方法将自己阻塞,此时计数器会减1,当计数器减为0的时候所有因调用await()方法而被阻塞的线程将被唤醒。这就是实现一组线程相互等待的原理,下面我们先看看CyclicBarrier有哪些成员变量:
    private static class Generation {   //静态内部类Generation🧡
        Generation() {}     
        //用来标志这一代是否被破坏           
        boolean broken;              
    }
    /** 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 每次换代前(计数器为0时)要执行的任务🧡 */
    private final Runnable barrierCommand;
    /** The current generation 栅栏的当前代🧡*/
    private Generation generation = new Generation();
    //计数器🧡
    private int count;
  • 我们再看一下CyclicBarrier中的所有成员变量:
    • trip: CyclicBarrier内部通过条件队列trip来对线程进行阻塞的;
    • parties: 表示每次拦截的线程数,该值在初始化CyclicBarrier时进行赋值;
      • count: 内部计数器,它的初始值和parties相同,以后随着每次await()方法的调用而减1,直到减为0就将所有线程唤醒,然后执行任务(也可不执行),换代等操作;
    • Generation: CyclicBarrier中的静态内部类Generation,该类的对象代表栅栏的当前代,每次count0就更新换代,利用它可以实现循环等待
    • barrierCommand: 表示换代前执行的任务,当count减为0时表示要换代了。在换代之前会将所有阻塞的线程唤醒,在唤醒所有线程之前你可以通过指定barrierCommand来执行自己的任务
    • 我们通过一张图来进一步了解(转载的图):
      在这里插入图片描述

循环栅栏CyclicBarrier的函数列表

//初始化一个新的CyclicBarrier,并给定一个初始值,意义为一代可通过的线程数🧡
CyclicBarrier(int parties)
//创建一个新的 CyclicBarrier,并每次更新换代的时候,在唤醒阻塞的线程前要先执行这个barrierAction的任务,没有传null🧡
CyclicBarrier(int parties, Runnable barrierAction)
//所有线程要在栅栏前等待🧡
int await()
//所有线程要在栅栏前等待,如果超出了指定的等待时间就不等了🧡。
int await(long timeout, TimeUnit unit)
//返回当前在屏障处等待的参与者数目🧡。
int getNumberWaiting()
//返回要求启动此 barrier 的参与者数目🧡。
int getParties()
//查询此屏障是否处于损坏状态🧡。
boolean isBroken()
//将屏障重置为其初始状态,就是将count== parties🧡。
void reset()

循环栅栏CyclicBarrier的使用

  • 我们通过两个实际中的例子来进一步理解怎么使用这个CyclicBarrier
    • 当司令下达命令,要求10个士兵一起去完成一项任务,这时需要他们先集合报道,然后去执行任务,当10个士兵把各自任务都执行完了,司令才对外宣布,任务完成!
    • 吃饭时要等全家人都上座了才动筷子,旅游时要等全部人都到齐了才出发,比赛时要等运动员都上场后才开始。
  • 接下来我们通过程序看到底如何简单使用CyclicBarrier:
package com.wwj.lockdemos;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierDemo {
    private  static final int size = 5;
    private  static CyclicBarrier cyclicBarrier;

    static  class  InnerThread extends  Thread{
        @Override
        public void run() {
            try{
                System.out.println(Thread.currentThread().getName()+"等待栅栏");
                cyclicBarrier.await();
                System.out.println(Thread.currentThread().getId()+"continue");
            }catch (InterruptedException e){
                e.printStackTrace();
            }catch (BrokenBarrierException t){
                t.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        cyclicBarrier  = new CyclicBarrier(size);
        for(int i=0 ; i<5 ; i++){
            new InnerThread().start();
        }
    }
}
结果:
Thread-1等待栅栏
Thread-3等待栅栏
Thread-0等待栅栏
Thread-4等待栅栏
Thread-2等待栅栏
17continue
15continue
18continue
16continue
19continue
  • 结果分析:
    • 主线程中新建了5个线程,所有的这些线程都调用cyclicBarrier.await()等待。所以这些线程一直等待,直到cyclicBarrier中所有线程都达到barrier时,这些线程才继续运行!
  • 那如何突出循环二字呢?我们再看另一个程序,也就是我们开始举的第一个例子,士兵命令士兵完成任务。(先集合,全都执行完任务)这两次都需要循环栅栏的实现:
package com.wwj.lockdemos;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierDemo1 {
    //先写士兵要执行的事情🧡
    public  static class  Solider implements Runnable {
        static   int i=0;
        private CyclicBarrier barrier ;
        private  String name;
        //构造器
        public Solider(CyclicBarrier barrier , String name){ //要通过构造器将CyclicBarrier关联起来🧡;
            this.barrier = barrier;
            this.name = name;
        }
        //执行任务
        @Override
        public void run() {
            //先集合
            try{
                //士兵先集合
                barrier.await();
                //集合完执行任务
                DoWork();
                //执行完任务集合,这也是循环的运用,并且我们想让他们每次执行更新代前都发出相应的指令🧡
                barrier.await();
            }catch (InterruptedException e){
                e.printStackTrace();
            }catch (BrokenBarrierException t){
                t.printStackTrace();
            }
        }
        static void DoWork(){
            try {
                System.out.print("士兵"+ ++i +":任务完成");
                Thread.sleep((int)Math.random()*1000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
    public static  class  BarrierRun implements  Runnable{
        //因为我们是两次循环,想让换代前发出不同的指令,通过flag控制
        private  boolean flag;
        private final int N;
        public BarrierRun(boolean flag , int N){  
            this.flag = flag;
            this.N = N;
        }
        @Override
        public void run() {
            if(flag){
                System.out.println();
                System.out.println("司令:[士兵"+N+"个,任务完成!]");
            }else{
                System.out.println();
                System.out.println("司令:[士兵"+N+"个,集合完毕!]");
                flag = true;
            }
        }
    }
    public  static void main(String[] args){
        final  int N=5;
        boolean flag = false;
        CyclicBarrier cyclicBarrier = new CyclicBarrier(5 , new BarrierRun(flag,N));
        Thread[] threads = new Thread[5];
        for(int i=0 ; i<5 ; i++){
            System.out.print("士兵"+(i+1)+":集合完毕  ");
            threads[i] = new Thread(new Solider(cyclicBarrier,"士兵"+i));
            threads[i].start();
        }
    }
}
//结果:
//士兵1:集合完毕  士兵2:集合完毕  士兵3:集合完毕  士兵4:集合完毕  士兵5:集合完毕  
//司令:[士兵5个,集合完毕!]
//士兵1:任务完成士兵2:任务完成士兵5:任务完成士兵4:任务完成士兵3:任务完成
//司令:[士兵5个,任务完成!]
  • 分析:
    • 我们可以看出集合完毕意味着CyclicBarrier的一次计数完成,当再一次调用CyclicBarrier.await()方法的时候,会进行下一代的计数。这次计数在程序中对应的就是显示士兵是否都完成了任务;
    • 程序中两个内部类都扩展了Runnable接口,一个是为当更新换代时先执行相应的操作,另一个是为了让线程执行,注意它们的区别;
    • CyclicBarrier.await()方法可能会抛出两个异常:
      • 一个是InterruptedException,也就是在等待过程中线程被中断;
      • 另一个是BrokenBarrierException异常,这个异常标识当前的CyclicBarrier已经破损了,我们通过程序具体说明其解决的问题:
      • 当我们把程序的一部分改成如下:
        for(int i=0 ; i<5 ; i++){
            System.out.print("士兵"+(i+1)+":集合完毕  ");
            threads[i] = new Thread(new Solider(cyclicBarrier,"士兵"+i));
            threads[i].start();
            if(i==2){  //添加的地方
                threads[0].interrupt();
            }
        }
 //结果一部分: java.util.concurrent.BrokenBarrierException
  • 分析:为什么会这样呢?
    • 这样做,我们很可能会抛出1个InterruptedException,4个BrokenBarrierExceptionInterruptedException是被中断线程抛出的,而其他4个BrokenBarrierException则是等待在当前CyclicBarrier上的线程抛出的。这个异常可以避免其他4个线程进行永久的、无谓的等待(因为其中一个中断了,没有结果)。
  • 接下我们看其源码,进一步理解CyclicBarrier的使用。

循环栅栏CyclicBarrier的实现原理

  • 首先我们看一下它的构造器:
   //构造器1
   public CyclicBarrier(int parties) {
       this(parties, null);
   }
   //构造器2
   public CyclicBarrier(int parties, Runnable barrierAction) {
       if (parties <= 0) throw new IllegalArgumentException();
       this.parties = parties;
       this.count = parties;
       this.barrierCommand = barrierAction;
   }
  • 通过构造器我们发现:
    • CyclicBarrier初始化时本质调用的是构造器2,当更新换代时要先执行barrierAction中的任务,然后才能换代,同时将计数器count的值重新设置为parties;
  • 接下来我们再看CyclicBarrier中最重要的功能—>等待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));
   }
  • CyclicBarrier类最主要的功能就是使先到达屏障点的线程阻塞并等待后面的线程,其中它提供了两种等待的方法,分别是定时等待和非定时等待。
  • 通过这两个方法,我们可以看到不管是定时等待还是非定时等待,它们都调用了dowait()方法,只不过是传入的参数不同而已。
  • 下面我们就来看看dowait()方法;

重点看dowait()方法的源码

    private int dowait(boolean timed, long nanos)
        throws InterruptedException, BrokenBarrierException,
               TimeoutException {
        final ReentrantLock lock = this.lock;
        //重入锁锁定共享资源🧡
        lock.lock();
        try {
        	 //当前代,也就是当前栅栏包含parties个线程的当前代🧡
        	 //这个代并不是初始化得到的,而是通过nextGeneration()设置的generation🧡
            final Generation g = generation;
			 //检查当前栅栏是否被打破🧡
            if (g.broken)
                throw new BrokenBarrierException();
			//检查当前线程是否被中断,如果中断了,我们则打破这个栅栏,更新换代需要手动🧡
            if (Thread.interrupted()) {
                breakBarrier();
                throw new InterruptedException();
              /*
                private void breakBarrier() {🧡
      			  generation.broken = true;
      			  count = parties;
      			  trip.signalAll();
 			   }
 			   我们看一下线程中断会有什么对应的操作:🧡🧡🧡   
 			   1、打破当前栅栏;🧡
 			   2、唤醒等待队列中所有的线程,trip就是通过Condition建立的等待队列;🧡
 			   3、将计数器重新赋值,也就是parties;🧡
            */
            }
            //如果没有中断,则将计数器减1🧡
            int index = --count;
            //计数器的值减为0则需唤醒所有线程并更新换代🧡
            if (index == 0) {  // tripped
            	 //唤醒所有线程前 先 执行指定的任务🧡
                Runnable command = barrierCommand;
                //因为有两个构造器,可以将指定的任务设为null,不为null执行下面操作,否则跳过🧡
                if (command != null) {
                    try {
                        command.run();
                    } catch (Throwable ex) {
                    	 //如果任务没有完成就中断了,则打破这个栅栏🧡
                        breakBarrier();
                        throw ex;
                    }
                }
               /*
                	//我们发现这个方法和breakBarrier()方法差不多🧡🧡🧡
                	1、唤醒等待队列中所有的线程,trip就是通过Condition建立的等待队列;🧡
                	2、将计数器重新赋值,也就是parties;🧡
                	3、更新换代🧡
                    private void nextGeneration() { 
     			  	  trip.signalAll();  
      				  count = parties;
      			  	  generation = new Generation();
    			}
              */
                nextGeneration();
                return 0;
            }
            //如果计数器不为0则执行此循环,如果中断或者时间超时则退出🧡
            for (;;) {
                try {
                 //根据传入的参数来决定是定时等待还是非定时等待🧡
                    if (!timed)  //false
                        trip.await();
                    else if (nanos > 0L) //true
                        nanos = trip.awaitNanos(nanos);
                } catch (InterruptedException ie) {
                	 //若当前线程在等待期间被中断则打翻栅栏唤醒其他线程🧡
                    if (g == generation && ! g.broken) {
                        breakBarrier();
                        throw ie;
                    } else {
                     //若在捕获中断异常前已经完成在栅栏上的等待, 则直接调用中断操作🧡
                        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()这个方法:
    • 每次都将count减1,减完后立马进行判断看看是否等于0:
      • 如果等于0的话就会先去执行之前指定好的任务,执行完之后再调用nextGeneration方法将栅栏转到下一代:
        • nextGeneration方法中会将所有线程唤醒,将计数器的值重新设为parties,最后更新换代。
        • 在执行完nextGeneration方法之后就意味着已经换代成功。
      • 如果计数器此时还不等于0的话就进入for循环,根据参数来决定是调用trip.awaitNanos(nanos)还是trip.await()方法,这两方法对应着定时和非定时等待;
      • 如果在等待过程中当前线程被中断就会执行breakBarrier方法,该方法叫做打破栅栏,设置generationbroken状态为true并唤醒所有线程。同时这也说明在等待过程中有一个线程被中断则这个“时代”就结束了,所有之前被阻塞的线程都会被唤醒。
    • 线程醒来后会执行下面三个判断:
      • 看看是否因为调用breakBarrier方法而被唤醒,如果是则抛出异常;
      • 看看是否是正常的换代操作而被唤醒,如果是则返回计数器的值;
      • 看看是否因为超时而被唤醒,如果是的话就调用breakBarrier打破栅栏并抛出异常;
    • 这里还需要注意的是,如果其中有一个线程因为等待超时而退出,其他线程都会被唤醒。

再看dowait()中一些其他方法的源码

	//打翻当前栅栏
    private void breakBarrier() {
    	//将当前栅栏状态设置为打翻
        generation.broken = true;
        //设置计数器的值为需要拦截的线程数
        count = parties;
        //唤醒所有线程
        trip.signalAll();
    }
      //更新换代
      private void nextGeneration() {
      //唤醒条件队列所有线程
      trip.signalAll();
     //设置计数器的值为需要拦截的线程数
      count = parties;
      //重新设置栅栏代次
      generation = new Generation();
  }
  • 我们发现这两个方法其实区别并不大:
    • 相同点:都重置了计数器count,然后唤醒所有的线程
    • 不同点:breakBarrier()将当代的栅栏状态设置为已破坏,nextGeneration()顾名思义创建了新的一代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();
        }
    }
  • 通过reset()源码我们可以发现:
    • 如果初始化时,指定了线程 parties = 4,前面有 3 个线程调用了 await 等待,在第 4 个线程调用 await 之前,我们调用 reset 方法,那么会发生什么?
    • 首先,打破栅栏,那意味着所有等待的线程(3个等待的线程)会唤醒,await() 方法会通过抛出 BrokenBarrierException 异常返回。然后开启新的一代,重置了 countgeneration,相当于一切归零了。
  • 到此,循环栅栏CyclicBarrier就介绍完了。
  • 文章部分内容参考如下点击!!!!!
  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值