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. Kibana使用仪表盘汇总数据(Dashboard)

    仪表板可用于集中管理和分享可视化控件集合.构建一个仪表板用以包含您在本教程中已保存的可视化控件,方法如下: 1.在侧边导航栏点击 Dashboard . 2.点击 Add 显示已保存的可视化控件列表. ...

  2. 1_Layui

    一. 引言 官网: https://www.layui.com/ 在官网首页, 可以很方便的下载Layui Layui是一款经典模块化前端UI框架, 我们只需要定义简单的HTML,CSS,JS即可实现 ...

  3. PAT (Basic Level) Practice 1005 继续(3n+1)猜想 分数 25

    卡拉兹(Callatz)猜想已经在1001中给出了描述.在这个题目里,情况稍微有些复杂. 当我们验证卡拉兹猜想的时候,为了避免重复计算,可以记录下递推过程中遇到的每一个数.例如对 n=3 进行验证的时 ...

  4. @input含义和用法

    @input :一般用于监听事件只要输入的值变化了就会触发input 示例: <div id="div1"> <input type="text&quo ...

  5. Mapper 实体转换Entiy to Dto

    实际使用中发现很多问题 如果用EFcore 框架,这个表达式树生成一个新的实体导致EFcore 跟踪失败!/// <summary> /// 生成表达式目录树 泛型缓存 /// </ ...

  6. 1、小程序Vant_WebApp组件库的安装步骤和简单使用

    Vant 1.小程序对于npm的支持 目前,小程序当中已经支持使用npm安装的第三方包,通过使用这些第三方包,我们可以提高对小程序开发的效率,但是在小程序当中使用所谓的npm包有如下的三个限制 不能支 ...

  7. 日志处理logging

    前言 什么是日志?有什么作用?日志是跟踪软件运行时所发生的事件的一种方法,简单来说它可以记录某时某刻运行了什么代码,当出现问题时可以方便我们进行定位. 由python内置了一个logging模块,用户 ...

  8. How to install the Package Controller

    How to install the Package Controller? https://packagecontrol.io/installation INSTALLATION Use one o ...

  9. Python基础之函数:4、二分法、三元表达式、生成/推导式、匿名函数、内置函数

    目录 一.算法简介之二分法 1.什么是算法 2.算法的应用场景 3.二分法 二.三元表达式 1.简介及用法 三.各种生成式 1.列表生成式 2.字典生成式 3.集合生成式 四.匿名函数 五.常见内置函 ...

  10. ES6 学习笔记(九)Set的基本用法

    1 基本用法 set类似于数组,它的成员是唯一的,当有多个相同的值,只会保留一份. 1.1 创建方法 Set本身是一个构造函数,用来生成Set实例,如: const s = new Set() let ...