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实现的。

CyclicBarrier循环屏障源码解析(基于jdk11)的更多相关文章

  1. String,StringBuffer和StringBuilder源码解析[基于JDK6]

    最近指导几位新人,学习了一下String,StringBuffer和StringBuilder类,从反馈的结果来看,总体感觉学习的深度不够,没有读出东西.其实,JDK的源码是越读越有味的.下面总结一下 ...

  2. 【Java并发集合】ConcurrentHashMap源码解析基于JDK1.8

    concurrentHashMap(基于jdk1.8) 类注释 所有的操作都是线程安全的,我们在使用时无需进行加锁. 多个线程同时进行put.remove等操作时并不会阻塞,可以同时进行,而HashT ...

  3. Spring源码解析-基于注解依赖注入

    在spring2.5版本提供了注解的依赖注入功能,可以减少对xml配置. 主要使用的是 AnnotationConfigApplicationContext: 一个注解配置上下文 AutowiredA ...

  4. Java 集合系列05之 LinkedList详细介绍(源码解析)和使用示例

    概要  前面,我们已经学习了ArrayList,并了解了fail-fast机制.这一章我们接着学习List的实现类——LinkedList.和学习ArrayList一样,接下来呢,我们先对Linked ...

  5. Java 集合系列03之 ArrayList详细介绍(源码解析)和使用示例

    概要 上一章,我们学习了Collection的架构.这一章开始,我们对Collection的具体实现类进行讲解:首先,讲解List,而List中ArrayList又最为常用.因此,本章我们讲解Arra ...

  6. Java 集合系列06之 Vector详细介绍(源码解析)和使用示例

    概要 学完ArrayList和LinkedList之后,我们接着学习Vector.学习方式还是和之前一样,先对Vector有个整体认识,然后再学习它的源码:最后再通过实例来学会使用它.第1部分 Vec ...

  7. Java 集合系列10之 HashMap详细介绍(源码解析)和使用示例

    概要 这一章,我们对HashMap进行学习.我们先对HashMap有个整体认识,然后再学习它的源码,最后再通过实例来学会使用HashMap.内容包括:第1部分 HashMap介绍第2部分 HashMa ...

  8. Java 集合系列 09 HashMap详细介绍(源码解析)和使用示例

    java 集合系列目录: Java 集合系列 01 总体框架 Java 集合系列 02 Collection架构 Java 集合系列 03 ArrayList详细介绍(源码解析)和使用示例 Java ...

  9. Java 集合系列 05 Vector详细介绍(源码解析)和使用示例

    java 集合系列目录: Java 集合系列 01 总体框架 Java 集合系列 02 Collection架构 Java 集合系列 03 ArrayList详细介绍(源码解析)和使用示例 Java ...

  10. Java 集合系列 04 LinkedList详细介绍(源码解析)和使用示例

    java 集合系列目录: Java 集合系列 01 总体框架 Java 集合系列 02 Collection架构 Java 集合系列 03 ArrayList详细介绍(源码解析)和使用示例 Java ...

随机推荐

  1. 清理rook-ceph

    官方步骤文档:https://rook.io/docs/rook/v1.8/ceph-teardown.html 请注意需要清理的以下资源: rook-ceph namespace: The Rook ...

  2. mysql8数据库修改root密码,以及创建用户遇到的坑,开启远程登录,用navicat进行mysql的远程连接,mysql8.0默认编码方式,部分参数配置查询命令

    yum 安装MySQL8 echo "删除系统默认或之前可能安装的其他版本的 mysql" for i in $(rpm -qa|grep mysql);do rpm -e $i ...

  3. 21. Fluentd输出插件:rewrite_tag_filter用法详解

    我们在做日志处理时,往往会从多个源服务器收集日志,然后在一个(或一组)中心服务器做日志聚合分析. 源服务器上的日志可能属于同一应用类型,也可能属于不同应用类型.我们可能需要在聚合服务器上对这些不同类型 ...

  4. 220722 T3 石子染色 (背包)

    序列s中的数就是要选的堆的编号,假设要选的有i个石子,这i个染为红色,剩下j个染为蓝色,i+j=x,i=x-j,那么对答案的贡献是|x-2j|.那么只要我们选的有i个石子,贡献就是这么多,所以我们可以 ...

  5. 洛谷P2602 [ZJOI2010] 数字计数 (数位DP)

    白嫖的一道省选题...... 1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 usin ...

  6. POJ2823 滑动窗口 (单调队列)

    来学习一下单调队列: 他只可以从队尾入队,但可以从队尾或队首出队,来维护队列的单调性.单调队列有两种单调性:元素的值单调和元素的下标单调. 单调队列可以用来优化DP.状态转移方程形如dp[i]=min ...

  7. 自主创建mybtis管理应用,用以横向管理数据源

    这个是我写的第一个随手小记,一晃眼做后端开发也有7年多了,现在也准备将一些杂七杂八的资料整理下.也算是回顾这7年中做的比较有意思的东西了. 这个需求是我17年做的,当时的应用场景是仓储库比较多,随时会 ...

  8. IDEA对数据库、表、记录的(增删改查可视化操作)、数据库安全性问题的演示

    对数据库的增删改查 新增数据库 修改数据库 删除数据库 对表的增删改查 新增表 修改表 删除表 对记录的增删改查 数据库安全性问题的演示 演示脏读 ​ 一个事物里面读到了另外一个事物没有提交的数据: ...

  9. LOJ2324「清华集训 2017」小Y和二叉树

    题目链接 瞎jb贪一发就过了.首先度数<=2且编号最小的点一定是中序遍历最靠前的点,我们从这个点开始dfs一遍算出子树中度数<=2且编号最小的点记为\(f(i)\),然后从这个点开始一步一 ...

  10. SQL---ltrim()和rtrim()函数的使用

    背景 去除字符串首尾空格大家肯定第一个想到trim()函数,不过在sqlserver中是没有这个函数的,却而代之的是ltrim()和rtrim()两个函数. 看到名字所有人都 知道做什么用的了,ltr ...