欢迎来到《并发王者课》,本文是该系列文章中的第21篇,铂金中的第8篇

在上一篇文章中,我们介绍了CountDownLatch的用法。在协调多线程的开始和结束时,CountDownLatch是个非常不错的选择。而本文即将给你介绍的CyclicBarrier则更加有趣,它在能力上和CountDownLatch既有相似之处,又有着明显的不同,值得你一览究竟。本文会先从场景上带你理解问题,再去理解CyclicBarrier提供的方案。

一、CyclicBarrier初体验

1. 峡谷森林里的爱情

在峡谷的江湖中,不仅有生杀予夺和刀光剑影,还有着美妙的爱情故事。

峡谷战神铠曾经在危急关头救了大乔,这一出英雄救美让他们擦除了爱情的火花,有事没事两人就在峡谷中的各个角落幽会。其中,峡谷森林就是他们常去的地方,谁先到就等另一个,两人都到齐后,再一起玩耍

这里头,有两个重点。一是他们要相互等待,二是都到齐后再玩耍。现在,我们试想一下,如果用代码来模拟这个场景的话,你打算怎么做。有的同学可能会说,两个人(线程)的等待很好处理。可是,如果是三人呢

所以,这个场景问题可以概括为:多个线程相互等待,到齐后再执行特定动作

接下来,我们就通过CyclicBarrier来模拟解决这个场景的问题,直观感受CyclicBarrier的用法。

在下面这段代码中,我们定义了一个幽会地点(appointmentPlace),以及大乔这两个主人公。在他们都达到幽会地点后,我们输出一句包含三朵玫瑰的话来予以确认,给他们送上祝福。

 private static String appointmentPlace = "峡谷森林";

 public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(2, () -> print("到达约会地点:大乔和铠都来到了" + appointmentPlace));
Thread 大乔 = newThread("大乔", () -> {
say("铠,你在哪里...");
try {
cyclicBarrier.await(); //到达幽会地点
say("铠,你终于来了...");
} catch (Exception e) {
e.printStackTrace();
}
}); Thread 铠 = newThread("铠", () -> {
try {
Thread.sleep(500); //铠打野中
say("我打个野,马上就到!");
cyclicBarrier.await(); //到达幽会地点
say("乔,不好意思,刚打野遇上兰陵王了,你还好吗?!");
} catch (Exception e) {
e.printStackTrace();
}
}); 大乔.start();
铠.start();
}

输出结果如下:

大乔:铠,你在哪里...
铠:我打个野,马上就到!
到达约会地点:大乔和铠都来到了峡谷森林
铠:乔,不好意思,刚打野遇上兰陵王了,你还好吗?!
大乔:铠,你终于来了... Process finished with exit code 0

对于代码的细节暂且不必深究,本文后面对CyclicBarrier的内部细节会有详解,先感受它的基本用法。

从结果中可以看到,CyclicBarrier可以像CountDownLatch一样,协调多线程的执行结束动作,在它们都结束后执行特定动作。从这点上来说,这是CyclicBarrier与CountDownLatch相似之处。然而,接下来的这个场景,所体现的则是它们一个明显的不同之处。

2. 小河边的幽会

在上面的场景中,铠已经提到他在打野时遇到了兰陵王。而在铠与大乔的约会中,兰陵王竟然又撞见了他们,真是冤家路窄。于是,在兰陵王的搅局下,铠和大乔不得不转移阵地,他们同样约定到新的约定地点后等待对方。(铠一直以为兰陵王也喜欢大乔,要和他横刀夺爱,其实兰陵王在乎的只是铠打了它的野,他的心里只有野怪,对任何女人毫无兴趣)。

此时,如果继续用代码模拟这一场景的话,那么CountDownLatch就无能为力了,因为CountDownLatch的使用是一次性的,无法重复利用。而此时,你就会发现CyclicBarrier的神奇之处,它竟然可以重复利用。似乎,你可能已经大概明白它为什么叫Cyclic的原因了。

接下来,我们再走一段代码,模拟大乔和铠的第二次幽会。在代码中,我们仍然定义幽会地点、大乔和铠两个主人公。但是与此前不同的是,我们还增加了兰陵王这个搅局者,以及中途变更了幽会地点

private static String appointmentPlace = "峡谷森林";

public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(2, () -> System.out.println("到达约会地点:大乔和铠都来到了" + appointmentPlace));
Thread 大乔 = newThread("大乔", () -> {
say("铠,你在哪里...");
try {
cyclicBarrier.await();
say("铠,你终于来了...");
Thread.sleep(2600); //约会中...
say("好的,你要小心!");
cyclicBarrier.await(); // 注意这里是第二次调用await
Thread.sleep(100);
say("真好!");
} catch (Exception e) {
e.printStackTrace();
}
}); Thread 铠 = newThread("铠", () -> {
try {
Thread.sleep(500); //铠打野中
say("我打个野,马上就到!");
cyclicBarrier.await(); //到达幽会地点
say("乔,不好意思,刚打野遇上兰陵王了,你还好吗?!");
Thread.sleep(1500); //幽会中... note("幽会中...\n"); Thread.sleep(1000); //幽会中...
say("这个该死的兰陵王!乔,你先走,小河边见!"); //铠突然看到了兰陵王
appointmentPlace = "小河边"; // 铠把地点改成了小河边 Thread.sleep(1500); //和兰陵王对决中...
note("︎\uD83D\uDDE1\uD83D\uDD2A铠和兰陵王决战开始,最终铠杀死了兰陵王,并前往小河边...\n");
cyclicBarrier.await(); // 杀了兰陵王后,铠到了小河边 !!!注意这里是第二次调用await say("乔,我已经解决了兰陵王,你看今晚夜色多美,我陪你看星星到天明...");
} catch (Exception ignored) {}
}); Thread 兰陵王 = newThread("兰陵王", () -> {
try {
Thread.sleep(2500);
note("兰陵王出场...");
say("铠打了我的野,不杀他誓不罢休!"); say("铠,原来你和大乔在这里!\uD83D\uDDE1️\uD83D\uDDE1️");
} catch (Exception ignored) {}
}); 兰陵王.start();
大乔.start();
铠.start();
}

输出结果如下所示。铠峡谷森林的好事被兰陵王搅局后,铠怒火中烧,让大乔先走,并约定在小河边碰面。随后,铠斩杀了兰陵王(可怜的钢铁直男),并前往小河边,完成他和大乔的第二次幽会

大乔:铠,你在哪里...
铠:我打个野,马上就到!
到达约会地点:大乔和铠都来到了峡谷森林
铠:乔,不好意思,刚打野遇上兰陵王了,你还好吗?!
大乔:铠,你终于来了...
幽会中... 兰陵王出场...
兰陵王:铠打了我的野,不杀他誓不罢休!
兰陵王:铠,原来你和大乔在这里!️️
铠:这个该死的兰陵王!乔,你先走,小河边见!
大乔:好的,你要小心!
︎铠和兰陵王决战开始,最终铠杀死了兰陵王,并前往小河边... 到达约会地点:大乔和铠都来到了小河边
铠:乔,我已经解决了兰陵王,你看今晚夜色多美,我陪你看星星到天明...
大乔:真好! Process finished with exit code 0

同样的,你暂时不要理会代码的细节,但是你要注意到其中铠和大乔对await()的两次调用。在你没有理解它的原理之前,可能会惊讶于它的神奇,这是正常现象。

二、CyclicBarrier是如何实现的

CyclicBarrier是Java中提供的一个线程同步工具,与CountDownLatch相似,但又并不完全相同,最核心的区别在于CyclicBarrier是可以循环使用的,这一点在它的名字中也已经有所体现

接下来,我们来分析下它具体的源码实现。

1. 核心数据结构

  • private final ReentrantLock lock = new ReentrantLock():进入屏障的锁,只有一把;
  • private final Condition trip = lock.newCondition():和上面的lock配套使用;
  • private final int parties:参与方的数量,本文上述的例子只有铠和大乔,所以数量是2;
  • private final Runnable barrierCommand:在本轮结束时运行的特定代码。本文上述例子用到了它,可以上翻查看;
  • private Generation generation = new Generation():当前屏障的代次。比如本文上述的两个场景中,generation是不同的,在铠和大乔将幽会地点改成小河边后,会生成新的generation;
  • private int count:正在等待的参与方数量。在每个代次中,count会从最初的参与数量(即parties)将至0,到0时本代次结束,而在新的代次或本代次被拆除(broken)时,count的值会恢复为parties的值。

2. 核心构造

  • public CyclicBarrier(int parties):指定参与方的数量;
  • public CyclicBarrier(int parties, Runnable barrierAction):指定参与方的数量,并指定在本代次结束时运行的代码。

3. 核心方法

  • public int await():如果当前线程不是第一个到达屏障的话,它将会进入等待,直到其他线程都到达,除非发生被中断屏障被拆除屏障被重设等情况;
  • public int await(long timeout, TimeUnit unit):和await()类似,但是加上了时间限制;
  • public boolean isBroken():当前屏障是否被拆除;
  • public void reset():重设当前屏障。会先拆除屏障再设置新的屏障;
  • public int getNumberWaiting():正在等待的线程数量。

在CyclicBarrier的各方法中,最为核心的就是dowait(),两个await()的内部都是调用这个方法。所以,理解了dowait(),基本上就理解了CyclicBarrier的实现关键。

dowait()方法略长,稍微需要点耐心,我已经对其中部分做了注释。当然,如果你想看源码的话,还是建议直接从JDK中看它的全部,这里的源码只是为了辅助你理解上下文。

private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock(); // 注意,这里是一定要加锁的
try {
final Generation g = generation; if (g.broken) // 如果当前屏障被拆除,则抛出异常
throw new BrokenBarrierException(); if (Thread.interrupted()) {
breakBarrier(); // 如果当前线程被中断,则拆除屏障并抛出异常
throw new InterruptedException();
} int index = --count; // 当线程调用await后,count减1
if (index == 0) { // tripped // 如果count为0,接下来将尝试结束屏障,并开启新的屏障
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
nextGeneration();
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
} // loop until tripped, broken, interrupted, or timed out
for (;;) {
try {
if (!timed)
trip.await();
else if (nanos > 0 L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
if (g == generation && !g.broken) {
breakBarrier();
throw ie;
} else {
// 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(); if (g != generation)
return index; if (timed && nanos <= 0 L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}

对于CyclicBarrier的核心数据结构、构造和方法,都在上面,它们很重要。但是,更为重要的是,要理解CyclicBarrier的思想,也就是下面这幅值得你收藏的图理解了这幅图,也就理解了CyclicBarrier.

此时,从这幅图再回头看第一节的两个场景,铠和大乔先后在峡谷森林小河边两个地点幽会。那么,如果也用一幅图来表示的话,它应该是下面这样:

三、CyclicBarrier与CountDownLatch有何不同

前面两节已经提到了两者的核心不同:

  • CountDownLatch是一次性的,而CyclicBarrier则可以多次设置屏障,实现重复利用
  • CountDownLatch中的各个子线程不可以等待其他线程,只能完成自己的任务;而CyclicBarrier中的各个线程可以等待其他线程

除此之外,它们俩还有着一些其他的不同,整体汇总后如下面的表格所示:

CyclicBarrier CountDownLatch
CyclicBarrier是可重用的,其中的线程会等待所有的线程完成任务。届时,屏障将被拆除,并可以选择性地做一些特定的动作。 CountDownLatch是一次性的,不同的线程在同一个计数器上工作,直到计数器为0.
CyclicBarrier面向的是线程数 CountDownLatch面向的是任务数
在使用CyclicBarrier时,你必须在构造中指定参与协作的线程数,这些线程必须调用await()方法 使用CountDownLatch是,则必须要指定任务数,至于这些任务由哪些线程完成无关紧要
CyclicBarrier可以在所有的线程释放后重新使用 CountDownLatch在计数器为0时不能再使用
在CyclicBarrier中,如果某个线程遇到了中断、超时等问题时,则处于await的线程都会出现问题 在CountDownLatch中,如果某个线程出现问题,其他线程不受影响

小结

以上就是关于CyclicBarrier的全部内容。在学习CyclicBarrier时,要侧重理解它所要解决的问题场景,以及它与CountDownLatch的不同,然后再去看源码,这也是为什么我们没有上来就放源码而是绕弯讲了个故事的原因,虽然那个故事挺“狗血”。当然,如果这个狗血的故事能让记住这个知识点,狗血也值得了。

正文到此结束,恭喜你又上了一颗星

夫子的试炼

  • 编写代码体验CyclicBarrier用法。

延伸阅读与参考资料

关于作者

关注【技术八点半】,及时获取文章更新。传递有品质的技术文章,记录平凡人的成长故事,偶尔也聊聊生活和理想。早晨8:30推送作者品质原创,晚上20:30推送行业深度好文。

如果本文对你有帮助,欢迎点赞关注监督,我们一起从青铜到王者

并发王者课-铂金8:峡谷幽会-看CyclicBarrier如何跨越重峦叠嶂的更多相关文章

  1. 并发王者课-铂金9:互通有无-Exchanger如何完成线程间的数据交换

    欢迎来到<并发王者课>,本文是该系列文章中的第22篇,铂金中的第9篇. 在前面的文章中,我们已经介绍了ReentrantLock,CountDownLatch,CyclicBarrier, ...

  2. 并发王者课-铂金10:能工巧匠-ThreadLocal如何为线程打造私有数据空间

    欢迎来到<并发王者课>,本文是该系列文章中的第23篇,铂金中的第10篇. 说起ThreadLocal,相信你对它的名字一定不陌生.在并发编程中,它有着较高的出场率,并且也是面试中的高频面试 ...

  3. 并发王者课-铂金1:探本溯源-为何说Lock接口是Java中锁的基础

    欢迎来到<并发王者课>,本文是该系列文章中的第14篇. 在黄金系列中,我们介绍了并发中一些问题,比如死锁.活锁.线程饥饿等问题.在并发编程中,这些问题无疑都是需要解决的.所以,在铂金系列文 ...

  4. 并发王者课-铂金6:青出于蓝-Condition如何把等待与通知玩出新花样

    欢迎来到<[并发王者课](https://juejin.cn/post/6967277362455150628)>,本文是该系列文章中的**第19篇**. 在上一篇文章中,我们介绍了阻塞队 ...

  5. 并发王者课-铂金2:豁然开朗-“晦涩难懂”的ReadWriteLock竟如此妙不可言

    欢迎来到<并发王者课>,本文是该系列文章中的第15篇. 在上篇文章中,我们介绍了Java中锁的基础Lock接口.在本文中,我们将介绍Java中锁的另外一个重要的基本型接口,即ReadWri ...

  6. 并发王者课 - 青铜 2:峡谷笔记 - 简单认识Java中的线程

    在前面的<兵分三路:如何创建多线程>文章中,我们已经通过Thread和Runnable直观地了解如何在Java中创建一个线程,相信你已经有了一定的体感.在本篇文章中,我们将基于前面的示例代 ...

  7. 并发王者课 - 青铜4:synchronized用法初体验

    在前面的文章<双刃剑-理解多线程带来的安全问题>中,我们提到了多线程情况下存在的线程安全问题.本文将以这个问题为背景,介绍如何通过使用synchronized关键字解这一问题.当然,在青铜 ...

  8. 并发王者课-青铜5:一探究竟-如何从synchronized理解Java对象头中的锁

    在前面的文章<青铜4:synchronized用法初体验>中,我们已经提到锁的概念,并指出synchronized是锁机制的一种实现.可是,这么说未免太过抽象,你可能无法直观地理解锁究竟是 ...

  9. 并发王者课-青铜7:顺藤摸瓜-如何从synchronized中的锁认识Monitor

    在前面的文章中,我们已经体验过synchronized的用法,并对锁的概念和原理做了简单的介绍.然而,你可能已经察觉到,有一个概念似乎总是和synchronized.锁这两个概念如影相随,很多人也比较 ...

随机推荐

  1. [bug] Flask:jinja2.exceptions.UndefinedError: 'None' has no attribute 'id'

    问题 Python Flask做的购物网站,添加购物车时,提示错误 解决 检查发现是MySQL中不正常的空数据导致,删除此条记录即可 参考 https://www.jb51.cc/python/186 ...

  2. 【转载】基于Linux命令行KVM虚拟机的安装配置与基本使用

    基于Linux命令行KVM虚拟机的安装配置与基本使用 https://alex0227.github.io/2018/06/06/%E5%9F%BA%E4%BA%8ELinux%E5%91%BD%E4 ...

  3. ip_conntrack or nf_conntrack : table full, dropping packet

    nf_conntrack: table full, dropping packet ip_conntrack or nf_conntrack : table full, dropping packet ...

  4. Linux进阶之Jenkins持续集成介绍及安装演示

    一.Jenkins介绍 Jenkins是一个开源软件项目,是基于Java开发的一种持续集成工具,用于监控持续重复的工作,旨在提供一个开放易用的软件平台,使软件的持续集成变成可能. Jenkins功能包 ...

  5. S5 Linux信息显示与搜索文件命令

    5.1-5 uname.hostname.dmesg.stat.du 5.6 date:显示与设置系统时间 5.7 echo:显示一行文本 5.8-12 watch.which.whereis.loc ...

  6. FD_SET -(转自 kakaxia6337的专栏)

    FD_ZERO,FD_ISSET这些都是套节字结合操作宏 看看MSDN上的select函数, 这是在select   io   模型中的核心,用来管理套节字IO的,避免出现无辜锁定. int   se ...

  7. 用JILINK 下载HEX文件

    https://wenku.baidu.com/view/f51300f55f0e7cd1842536e1.html

  8. Python中if name == 'main':的作用

    一个python文件通常有两种使用方法, 第一是作为脚本直接执行. 第二是 import 到其它的 python 脚 本中被调用(模块重用)执行. 因此 if name == 'main': 的作用就 ...

  9. 解决无法访问github的问题

    当我们想学习下载某个大神分享的github项目时,由于github域名解析异常,时常会无法访问Github网站. 下面是我总结分享的有效解决方法:思路是自己手动修改hosts文件添加域名解析! 下面教 ...

  10. C 语言通用模板队列

    前言 嵌入式开发过程中,各个模块之间,各个设备之间进行交互时,都会存在数据的输入输出,由于处理的方式不同,数据不会立即同步处理,因此通常在设计时都会设计缓冲区进行数据的处理,方式数据丢失等问题:一个项 ...