关注王有志,一个分享硬核Java技术的互金摸鱼侠

欢迎你加入Java人的提桶跑路群共同富裕的Java人

今天我们来学习AQS家族的“外门弟子”:CyclicBarrier。

为什么说CyclicBarrier是AQS家族的“外门弟子”呢?那是因为CyclicBarrier自身和内部类Generation并没有继承AQS,但在源码的实现中却深度依赖AQS家族的成员ReentrantLock。就像修仙小说中,大家族会区分外门和内门,外门弟子通常会借助内门弟子的名声行事,CyclicBarrier正是这样,因此算是AQS家族的“外门弟子”。在实际的面试中,CyclicBarrier的出现的次数较少,通常会出现在与CountDownLatch比较的问题当中

今天我们就逐步拆解CyclicBarrier,来看看它与CountDownLatch之间到底有什么差别。

CyclicBarrier是什么?

先从CyclicBarrier的名字开始入手,Cyclic是形容词,译为“循环的,周期的”,Barrier是名词,译为“屏障,栅栏”,组合起来就是“循环的屏障”,那么该怎么理解“循环的屏障”呢?我们来看CyclicBarrier的注释是怎么解释的:

A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point.

CyclicBarrier是一种同步辅助工具,允许一组线程等待彼此到达共同的屏障点。

The barrier is called cyclic because it can be re-used after the waiting threads are released.

因为在等待线程释放后可以重复使用,所以屏障被称为循环屏障。

看起来与CountDownLatch有些相似,我们通过一张图来展示下CyclicBarrier是怎样工作的:

部分线程到达屏障后,会在屏障处等待,只有全部线程都到达屏障后,才会继续执行。如果以CountDownLatch中越野徒步来举例的话,把老板拿掉,选手之间的互相等待,就是CyclicBarrier了。

另外,注释中说CyclicBarrier是“re-used”,即可重复使用的。回想一下CountDownLatch的实现,并未做任何重置计数器的工作,即当CountDownLatch的计数减为0后不能恢复,也就是说CountDownLatch的功能是一次性的

Tips:实际上,可以用CountDownLatch实现类似于CyclicBarrier的功能。

CyclicBarrier怎么用?

我们用没有老板参加的越野徒步来举例,部分先到的选手要等待后到的选手一起吃午饭,用CyclicBarrier来实现的代码是这样的:

// 初始化CyclicBarrier
CyclicBarrier cyclicBarrier = new CyclicBarrier(10); for (int i = 0; i < 10; i++) {
int finalI = i;
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep((finalI + 1));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
System.out.println("选手[" + finalI + "]到达终点,等待其他选手!!!"); // 线程在屏障点处等待
cyclicBarrier.await(); System.out.println("选手[" + finalI + "]开始吃午饭啦!!!");
} catch (InterruptedException | BrokenBarrierException e) {
throw new RuntimeException(e);
}
}).start();
}

用法和CountDownLatch很相似,构造函数设置CyclicBarrier需要多少个线程达到屏障后统一行动,区别是CyclicBarrier在每个线程中都调用了CyclicBarrier#await,而我们在使用CountDownLatch时只在主线程中调用了一次CountDownLatch#await

那CountDownLatch可以在线程中调用CountDownLatch#await吗?答案是可以的,这样使用的效果和CyclicBarrier是一样的:

CountDownLatch countDownLatch = new CountDownLatch(10);

for (int i = 0; i < 10; i++) {
int finalI = i;
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep((finalI + 1));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("选手[" + finalI + "]到达终点!!!");
countDownLatch.countDown();
try {
countDownLatch.await();
System.out.println("选手[" + finalI + "]开始吃午饭啦!!!");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
}

通过上面的例子,我们不难想到CyclicBarrier#await方法是同时具备了CountDownLatch#countDown方法和CountDownLatch#await方法的能力,即执行了计数减1,又执行了暂停线程

CyclicBarrier是怎么实现的?

我们先整体认识一下CyclicBarrier:

CyclicBarrier的内部结构比CountDownLatch复杂一些,除了我们前面提到的借助AQS的“内门弟子”ReentrantLock类型的lock和Condition类型的trip外,CyclicBarrier还有两个“特别”的地方:

  • 内部类Generation,直译过来是“代”,它起到什么作用?

  • Runnable类型的成员变量barrierCommand,它又做了些什么?

其余的部分,大部分可以在CountDownLatch中找到对应的方法,或者通过名称我们就很容易得知它们的作用。

CyclicBarrier的构造方法

CyclicBarrier提供了两个(实际是一个)构造方法:

// 需要到达屏障的线程数
private final int parties; // 所有线程都到达后执行的动作
private final Runnable barrierCommand; // 计数器
private int count; public CyclicBarrier(int parties) {
this(parties, null);
} public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) {
throw new IllegalArgumentException();
}
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}

第二个构造函数接收了两个参数:

  • parties:表示需要多少个线程到达屏障处调用CyclicBarrier#await

  • barrierAction:所有线程到达屏障后执行的动作。

构造方法的代码一如既往的简单,只有一处比较容易产生疑惑,partiescount有什么区别?

首先来看成员变量的声明,parties使用了final,表明它是不可变的对象,代表CyclicBarrier需要几个线程共同到达屏障处;而count是计数器,初始值是parties,随着到达屏障处的线程数量增多count会逐步减少至0。

CyclicBarrier的内部类Generation

private static class Generation {
Generation() {} boolean broken;
}

Generation用于标记CyclicBarrier的当前代,Doug Lea是这么解释它的作用的:

Each use of the barrier is represented as a generation instance. The generation changes whenever the barrier is tripped, or is reset.

每次使用屏障(CyclicBarrier)都需要一个Generation实例。无论是通过屏障还是重置屏障,Generation都会发生改变。

Generation中的broken用于标记当前的CyclicBarrier是否被打破,默认为false,值为true时表示当前CyclicBarrier已经被打破,此时CyclicBarrier不能正常使用,需要调用CyclicBarrier#reset方法重置CyclicBarrier的状态。

CyclicBarrier#await方法

前面我们猜测CyclicBarrier#await方法即实现了计数减1,又实现了线程等待的功能,下面我们就通过源码来验证我们的想法:

public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe);
}
} public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException {
return dowait(true, unit.toNanos(timeout));
}

两个重载方法都指向了CyclicBarrier#dowait方法:

private int dowait(boolean timed, long nanos)  throws InterruptedException, BrokenBarrierException, TimeoutException {
// 使用ReentrantLock
final ReentrantLock lock = this.lock;
lock.lock(); try {
// 第2部分
// 获取CyclicBarrier的当前代,并检查CyclicBarrier是否被打破
final Generation g = generation;
if (g.broken) {
throw new BrokenBarrierException();
} // 线程被中断时,调用breakBarrier方法
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
} // 第3部分
//计数器减1
int index = --count;
// 计数器为0时表示所有线程都到达了,此时要做的就是唤醒等待中的线程
if (index == 0) {
boolean ranAction = false;
try {
// 执行唤醒前的操作
final Runnable command = barrierCommand;
if (command != null) {
command.run();
}
ranAction = true;
// CyclicBarrier进入下一代
nextGeneration();
return 0;
} finally {
if (!ranAction) {
breakBarrier();
}
}
} // 第4部分
// 只有部分线程到达屏障处的情况
for (;;) {
try {
//调用等待逻辑)
if (!timed) {
trip.await();
} else if (nanos > 0L) {
nanos = trip.awaitNanos(nanos);
}
} catch (InterruptedException ie) {
// 线程被中断时,调用breakBarrier方法
if (g == generation && ! g.broken) {
breakBarrier();
throw ie;
} else {
Thread.currentThread().interrupt();
}
}
if (g.broken) {
throw new BrokenBarrierException();
}
// 如果不是当前代,返回计数器的值
if (g != generation) {
return index;
}
// 如果等待超时,调用breakBarrier方法
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}

CyclicBarrier#dowait方法看起来很长,但如果拆成3部分来看逻辑并不复杂:

  • 第1部分:CyclicBarrier与线程的状态校验;

  • 第2部分:当计数器减1后值为0时,唤醒所有等待中的线程;

  • 第3部分:当计数器减1后值不为0时,线程进入等待状态。

先来看第1部分,CyclicBarrier与线程的状态校验的部分,先是判断CyclicBarrier是否被打破,接着判断当前线程是否为中断状态,如果是则调用CyclicBarrier#breakBarrier方法:

private void breakBarrier() {
generation.broken = true;
count = parties;
trip.signalAll();
}

CyclicBarrier#breakBarrier方法非常简单,只做了3件事:

  • 标记CyclicBarrier被打破;

  • 重置CyclicBarrier的计数器;

  • 唤醒全部等待中的线程。

也就是说,一旦有个线程标记为中断状态,都会直接打破CyclicBarrier的屏障。

我们先跳过第2部分的唤醒逻辑,直接来看第3部分线程进入等待状态的逻辑。根据timed参数选择调用Condition不同的等待方法,随后是对异常的处理和线程中断状态的处理,同样是调用CyclicBarrier#breakBarrier,标记CyclicBarrier不可用。线程进入等待状态的逻辑并不复杂,本质上是通过AQS的Condition来实现的。

最后来看第2部分唤醒所有等待中线程的操作,根据计数器是否为0判断是否需要进行唤醒。如果需要唤醒,最后一个执行CyclicBarrier#await的线程执行barrierCommand(此时尚未执行任何线程唤醒的操作),做通过屏障前的处理操作,接着调用CyclicBarrier#nextGeneration方法:

private void nextGeneration() {
trip.signalAll();
count = parties;
generation = new Generation();
}

CyclicBarrier#nextGeneration方法也做了3件事:

  • 唤醒所有Condition上等待的线程;

  • 重置CyclicBarrier的计数器;

  • 创建新的Generation对象。

很符合进入“下一代”的名字,先唤醒“上一代”所有等待中的线程,然后重置CyclicBarrier的计数器,最后更新CyclicBarrier的Generation对象,对CyclicBarrier进行重置工作,让CyclicBarrier进入下一个纪元。

到这里我们不难发现,CyclicBarrier自身只做了维护计数器和重置计数器的工作,而保证互斥性和线程的等待与唤醒则是依赖AQS家族的成员完成的:

  • ReentrantLock保证了同一时间只有一个线程可以执行CyclicBarrier#await,即同一时间只有一个线程可以维护计数器;

  • Condition为CyclicBarrier提供了条件等待队列,完成了线程的等待与唤醒的工作。

CyclicBarrier#reset方法

最后我们来看CyclicBarrier#reset方法:

public void reset() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 主动打破CyclicBarrier
breakBarrier();
// 使CyclicBarrier进入下一代
nextGeneration();
} finally {
lock.unlock();
}
}

CyclicBarrier#reset方法都是老面孔,先是CyclicBarrier#breakBarrier打破上一代CyclicBarrier,既然要重新开始就不要再“怀念”过去了;最后调用CyclicBarrier#nextGeneration开始新的时代。需要注意的是,这里加锁的目的是为了保证执行CyclicBarrier#reset时,没有任何线程正在执行CyclicBarrier#await方法。

好了,到这里CyclicBarrier的核心内容我们就一起分析完了,剩下的方法就非常简单了,相信通过名字大家就可以了解它们的作用,并猜到它们的实现了。

TipsCyclicBarrier#getNumberWaiting中加了锁,这是为什么?

CountDownLatch和Cyclicbarrier有什么区别?

最后的部分,我们来解答下开篇时的面试题,CountDownLatch和Cyclicbarrier有什么区别?

第1点:CyclicBarrier可以重复使用,CountDownLatch不能重复使用

无论是正常使用结束,还是调用CyclicBarrier#reset方法,Cyclicbarrier都可以重置内部的计数器

第2点:Cyclicbarrier只阻塞调用CyclicBarrier#await方法的线程,而CountDownLatch可以阻塞任意一个或多个线程

CountDownLatch将计数减1与阻塞拆分成了CountDownLatch#countDownCountDownLatch#await两个方法,而Cyclicbarrier只通过CyclicBarrier#await完成两步操作。如果在同一个线程中连续CountDownLatch#countDownCountDownLatch#await则实现了与CyclicBarrier#await方法相同的功能。

结语

好了,今天就到这里结束了。如果本文对你有帮助的话,请多多点赞支持。最后欢迎大家关注分享硬核技术的金融摸鱼侠王有志,我们下次再见!

20.AQS家族的“外门弟子”:CyclicBarrier的更多相关文章

  1. AQS之CountDownLatch、Semaphore、CyclicBarrier

    CountDownLatch A synchronization aid that allows one or more threads to wait until a set of operatio ...

  2. 一个iOS开发者的修真之路

    在微信上有童鞋问我iOS开发者的入门标准是神马?这个问题难到我了,而且贸然给一个答案出来的话,必定会有万千高手来喷. 凡人修仙,仙人修道,道人修真.当我们还是一个在青石板上蹲马步汗水涔涔的废柴时,或许 ...

  3. 【开源】使用Angular9和TypeScript开发RPG游戏

    RPG系统构造 通过对于斗罗大陆小说的游戏化过程,熟悉Angular的结构以及使用TypeScript的面向对象开发方法. 项目地址 人物 和其他RPG游戏类似,游戏里面的人物角色大致有这样的一些属性 ...

  4. 【开源】使用Angular9和TypeScript开发RPG游戏(补充了Buffer技能)

    RPG系统构造 通过对于斗罗大陆小说的游戏化过程,熟悉Angular的结构以及使用TypeScript的面向对象开发方法. Github项目源代码地址 RPG系统构造 ver0.02 2020/03/ ...

  5. 爬虫入门到放弃系列02:html网页如何解析

    前言 上一篇文章讲了爬虫的概念,本篇文章主要来讲述一下如何来解析爬虫请求的网页内容. 一个简单的爬虫程序主要分为两个部分,请求部分和解析部分.请求部分基本一行代码就可以搞定,所以主要来讲述一下解析部分 ...

  6. 10月20日MySQL数据库作业解析

    设有一数据库,包括四个表:学生表(Student).课程表(Course).成绩表(Score)以及教师信息表(Teacher).四个表的结构分别如表1-1的表(一)~表(四)所示,数据如表1-2的表 ...

  7. 利用机器学习检测HTTP恶意外连流量

    本文通过使用机器学习算法来检测HTTP的恶意外连流量,算法通过学习恶意样本间的相似性将各个恶意家族的恶意流量聚类为不同的模板.并可以通过模板发现未知的恶意流量.实验显示算法有较好的检测率和泛化能力. ...

  8. 谈论高并发(三十)解析java.util.concurrent各种组件(十二) 认识CyclicBarrier栅栏

    这次谈话CyclicBarrier栅栏,如可以从它的名字可以看出,它是可重复使用. 它的功能和CountDownLatch类别似,也让一组线程等待,然后开始往下跑起来.但也有在两者之间有一些差别 1. ...

  9. 多线程之倒计时器CountDownLatch和循环栅栏CyclicBarrier

    1.倒计时器CountDownLatch CountDownLatch是一个多线程控制工具类.通常用来控制线程等待,它可以让一个线程一直等待知道计时结束才开始执行 构造函数: public Count ...

  10. JUC并发工具包之CyclicBarrier & CountDownLatch的异同

    1.介绍 本文我们将比较一下CyclicBarrier和CountDownLatch并了解两者的相似与不同. 2.两者是什么 当谈到并发,将这两者概念化的去解释两者是做什么的,这其实是一件很有挑战的事 ...

随机推荐

  1. 这年头,谁的好友列表还没有躺一个ChatGPT啊?

    你要是说这个,我可不困了 大家好,我最近开始使用一款非常有趣的AI机器人,它叫做ChatGPT.ChatGPT是一款独特的聊天机器人,它可以进行智能对话,回答你的问题,还可以学习你的语言习惯,使得对话 ...

  2. AcWing 1902. 马拉松

    题目链接 每次路程改变只对前后两点间距离有影响,因此每次都判断当前三个点之间的距离之和与去掉中间点的距离哪个更优即可,最后取最大值作为结果输出. #include<iostream> #i ...

  3. [操作系统]记一次未尽的三星 Galaxy A6s(SM-G6200)刷机过程

    给女王大人刷机,第一次刷机,很遗憾,遇到了三星的"锁三键"问题,没有搞成.记录一下这个过程所涉猎的一些刷机基本知识,不妨当作一次学习过程. 1 刷机过程 Step1 查看手机基本信 ...

  4. Nvidia GPU虚拟化

    1 背景 随着Nvidia GPU在渲染.编解码和计算领域发挥着越来越重要的作用,各大软件厂商对于Nvidia GPU的研究也越来越深入,尽管Nvidia倾向于生态闭源,但受制于极大的硬件成本压力,提 ...

  5. YII2.0框架分页

    这篇文章主要介绍了Yii分页用法,以实例形式详细分析了比较常见的几种分页方法及其应用特点,非常具有实用价值,需要的朋友可以参考下: 在这里我主要联查的 book 表和 book_press 两张表进行 ...

  6. 基于RL(Q-Learning)的迷宫寻路算法

    强化学习是一种机器学习方法,旨在通过智能体在与环境交互的过程中不断优化其行动策略来实现特定目标.与其他机器学习方法不同,强化学习涉及到智能体对环境的观测.选择行动并接收奖励或惩罚.因此,强化学习适用于 ...

  7. 23.oneOf

    const { resolve } = require('path') const HtmlWebpackPlugin = require('html-webpack-plugin') // 提取cs ...

  8. 为什么数据库project被做成了web开发啊啊——一个半小时实现增删查改

    昨天晚上去小破站上找了一点点~~亿点点~~资料,仔细研究了一下我们项目说明文档里的restful框架,发现可以直接用django_restful_framework. 天大的好消息啊!今天下午有三个小 ...

  9. 快速上手Linux核心命令(七):Linux系统信息相关命令

    目录 前言 uname 显示系统信息 hostname 显示或设置系统主机名 du 统计磁盘空间使用情况 echo 显示一行文本 watch 监视命令执行情况 stat whereis 显示命令及其相 ...

  10. 学习笔记——树形dp

    树形 dp 介绍 概念 树形 dp,顾名思义,就是在树上做 dp,将 dp 的思想建立在树状结构之上. 常见的树形 dp 有两种转移方向: 从叶节点向根节点转移,这种也是树形 dp 中较为常见的一种. ...