写点什么

并发王者课 - 铂金 8:峡谷幽会 - 看 CyclicBarrier 如何跨越重峦叠嶂

发布于: 22 小时前
并发王者课-铂金8:峡谷幽会-看CyclicBarrier如何跨越重峦叠嶂

欢迎来到《并发王者课》,本文是该系列文章中的第 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 的全部内容。在学习 CyclicBarrier 时,要侧重理解它所要解决的问题场景,以及它与 CountDownLatch 的不同,然后再去看源码,这也是为什么我们没有上来就放源码而是绕弯讲了个故事的原因,虽然那个故事挺“狗血”。当然,如果这个狗血的故事能让你记住这个知识点,狗血也值得了。


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


夫子的试炼


  • 编写代码体验 CyclicBarrier 用法。


延伸阅读与参考资料



关于作者


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


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

发布于: 22 小时前阅读数: 9
用户头像

微信公众号:【技术八点半】 2018.05.13 加入

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

评论

发布
暂无评论
并发王者课-铂金8:峡谷幽会-看CyclicBarrier如何跨越重峦叠嶂