Java高并发编程基础三大利器之CyclicBarrier

开发 后端
前面一篇文章我们《Java高并发编程基础三大利器之CountDownLatch》它有一个缺点,就是它的计数器只能够使用一次,也就是说当计数器(state)减到为 0的时候,如果 再有线程调用去 await() 方法,该线程会直接通过,不会再起到等待其他线程执行结果起到同步的作用。

 [[388240]]

引言

前面一篇文章我们《Java高并发编程基础三大利器之CountDownLatch》它有一个缺点,就是它的计数器只能够使用一次,也就是说当计数器(state)减到为 0的时候,如果 再有线程调用去 await() 方法,该线程会直接通过,不会再起到等待其他线程执行结果起到同步的作用。为了解决这个问题CyclicBarrier就应运而生了。

什么是CyclicBarrier

CyclicBarrier是什么?把它拆开来翻译就是循环(Cycle)和屏障(Barrier)

它的主要作用其实和CountDownLanch差不多,都是让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障会被打开,所有被屏障阻塞的线程才会继续执行,不过它是可以循环执行的,这是它与CountDownLanch最大的不同。CountDownLanch是只有当最后一个线程把计数器置为0的时候,其他阻塞的线程才会继续执行。学习CyclicBarrier之前建议先去看看这几篇文章:

Java高并发编程基础之AQS

Java高并发编程基础三大利器之Semaphore

Java高并发编程基础三大利器之CountDownLatch

如何使用

我们首先先来看下关于使用CyclicBarrier的一个demo:比如游戏中有个关卡的时候,每次进入下一关的时候都需要进行加载一些地图、特效背景音乐什么的只有全部加载完了才能够进行游戏:

  1. /**demo 来源https://blog.csdn.net/lstcui/article/details/107389371 
  2.  * 公众号【java金融】 
  3.  */ 
  4. public class CyclicBarrierExample { 
  5.     static class PreTaskThread implements Runnable { 
  6.         private String task; 
  7.         private CyclicBarrier cyclicBarrier; 
  8.  
  9.         public PreTaskThread(String task, CyclicBarrier cyclicBarrier) { 
  10.             this.task = task; 
  11.             this.cyclicBarrier = cyclicBarrier; 
  12.         } 
  13.  
  14.         @Override 
  15.         public void run() { 
  16.             for (int i = 0; i < 4; i++) { 
  17.                 Random random = new Random(); 
  18.                 try { 
  19.                     Thread.sleep(random.nextInt(1000)); 
  20.                     System.out.println(String.format("关卡 %d 的任务 %s 完成", i, task)); 
  21.                     cyclicBarrier.await(); 
  22.                 } catch (InterruptedException | BrokenBarrierException e) { 
  23.                     e.printStackTrace(); 
  24.                 } 
  25.             } 
  26.         } 
  27.  
  28.         public static void main(String[] args) { 
  29.             CyclicBarrier cyclicBarrier = new CyclicBarrier(3, () -> { 
  30.                 System.out.println("本关卡所有的前置任务完成,开始游戏... ..."); 
  31.             }); 
  32.             new Thread(new PreTaskThread("加载地图数据", cyclicBarrier)).start(); 
  33.             new Thread(new PreTaskThread("加载人物模型", cyclicBarrier)).start(); 
  34.             new Thread(new PreTaskThread("加载背景音乐", cyclicBarrier)).start(); 
  35.         } 
  36.     } 

输出结果如下:

我们可以看到每次游戏开始都会等当前关卡把游戏的人物模型,地图数据、背景音乐加载完成后才会开始进行游戏。并且还是可以循环控制的。

源码分析

结构组成

  1. /** The lock for guarding barrier entry */ 
  2. private final ReentrantLock lock = new ReentrantLock(); 
  3. /** Condition to wait on until tripped */ 
  4. private final Condition trip = lock.newCondition(); 
  5. /** The number of parties */ 
  6. private final int parties; 
  7. /* The command to run when tripped */ 
  8. private final Runnable barrierCommand; 
  9. /** The current generation */ 
  10. private Generation generation = new Generation(); 
  • lock:用于保护屏障入口的锁
  • trip :达到屏障并且不能放行的线程在trip条件变量上等待
  • parties :栅栏开启需要的到达线程总数
  • barrierCommand:最后一个线程到达屏障后执行的回调任务
  • generation:这是一个内部类,通过它实现CyclicBarrier重复利用,每当await达到最大次数的时候,就会重新new 一个,表示进入了下一个轮回。里面只有一个boolean型属性,用来表示当前轮回是否有线程中断。

主要方法

  1. public int await() throws InterruptedException, BrokenBarrierException { 
  2.     try { 
  3.         return dowait(false, 0L); 
  4.     } catch (TimeoutException toe) { 
  5.         throw new Error(toe); // cannot happen 
  6.     } 
  7.  
  8.  * Main barrier code, covering the various policies. 
  9.  */ 
  10. private int dowait(boolean timed, long nanos) 
  11.     throws InterruptedException, BrokenBarrierException, 
  12.            TimeoutException { 
  13.     final ReentrantLock lock = this.lock; 
  14.     lock.lock(); 
  15.      try { 
  16.            //获取barrier当前的 “代”也就是当前循环 
  17.          final Generation g = generation; 
  18.         if (g.broken) 
  19.             throw new BrokenBarrierException(); 
  20.  
  21.         if (Thread.interrupted()) { 
  22.             breakBarrier(); 
  23.             throw new InterruptedException(); 
  24.         } 
  25.         // 每来一个线程调用await方法都会进行减1 
  26.         int index = --count; 
  27.         if (index == 0) {  // tripped 
  28.             boolean ranAction = false
  29.             try { 
  30.                 final Runnable command = barrierCommand; 
  31.                 // new CyclicBarrier 传入 的barrierCommand, command.run()这个方法是同步的,如果耗时比较多的话,是否执行的时候需要考虑下是否异步来执行。 
  32.                 if (command != null
  33.                     command.run(); 
  34.                 ranAction = true
  35.                 // 这个方法1. 唤醒所有阻塞的线程,2. 重置下countcount 每来一个线程都会进行减1)和generation,以便于下次循环。 
  36.                 nextGeneration(); 
  37.                 return 0; 
  38.             } finally { 
  39.                 if (!ranAction) 
  40.                     breakBarrier(); 
  41.             } 
  42.         } 
  43.  
  44.         // loop until tripped, broken, interrupted, or timed out 
  45.         for (;;) { 
  46.             try { 
  47.                  // 进入if条件,说明是不带超时的await 
  48.                 if (!timed) 
  49.                      // 当前线程会释放掉lock,然后进入到trip条件队列的尾部,然后挂起自己,等待被唤醒。 
  50.                     trip.await(); 
  51.                 else if (nanos > 0L) 
  52.                      //说明当前线程调用await方法时 是指定了 超时时间的! 
  53.                     nanos = trip.awaitNanos(nanos); 
  54.             } catch (InterruptedException ie) { 
  55.                  //Node节点在 条件队列内 时 收到中断信号时 会抛出中断异常! 
  56.                 //g == generation 成立,说明当前代并没有变化。 
  57.                 //! g.broken 当前代如果没有被打破,那么当前线程就去打破,并且抛出异常.. 
  58.                 if (g == generation && ! g.broken) { 
  59.                     breakBarrier(); 
  60.                     throw ie; 
  61.                 } else { 
  62.                     // We're about to finish waiting even if we had not 
  63.                     // been interrupted, so this interrupt is deemed to 
  64.                     // "belong" to subsequent execution. 
  65.                 //执行到else有几种情况? 
  66.                 //1.代发生了变化,这个时候就不需要抛出中断异常了,因为 代已经更新了,这里唤醒后就走正常逻辑了..只不过设置下 中断标记。 
  67.                 //2.代没有发生变化,但是代被打破了,此时也不用返回中断异常,执行到下面的时候会抛出  brokenBarrier异常。也记录下中断标记位。 
  68.                     Thread.currentThread().interrupt(); 
  69.                 } 
  70.             } 
  71.            //唤醒后,执行到这里,有几种情况? 
  72.           //1.正常情况,当前barrier开启了新的一代(trip.signalAll()) 
  73.           //2.当前Generation被打破,此时也会唤醒所有在trip上挂起的线程 
  74.           //3.当前线程trip中等待超时,然后主动转移到 阻塞队列 然后获取到锁 唤醒。 
  75.             if (g.broken) 
  76.                 throw new BrokenBarrierException(); 
  77.            //唤醒后,执行到这里,有几种情况? 
  78.         //1.正常情况,当前barrier开启了新的一代(trip.signalAll()) 
  79.         //2.当前线程trip中等待超时,然后主动转移到 阻塞队列 然后获取到锁 唤醒。 
  80.             if (g != generation) 
  81.                 return index
  82.            //唤醒后,执行到这里,有几种情况? 
  83.         //.当前线程trip中等待超时,然后主动转移到 阻塞队列 然后获取到锁 唤醒。 
  84.             if (timed && nanos <= 0L) { 
  85.                 breakBarrier(); 
  86.                 throw new TimeoutException(); 
  87.             } 
  88.         } 
  89.     } finally { 
  90.          lock.unlock(); 
  91.     } 

小结

到了这里我们是不是可以知道为啥CyclicBarrier可以进行循环计数?

  • CyclicBarrier采用一个内部类Generation来维护当前循环,每一个await方法都会存储当前的generation,获取到相同generation对象的属于同一组,每当count的次数耗尽就会重新new一个Generation并且重新设置count的值为parties,表示进入下一次新的循环。

从这个await方法我们是不是可以知道只要有一个线程被中断了,当代的 generation的broken 就会被设置为true,所以会导致其他的线程也会被抛出BrokenBarrierException。相当于一个失败其他也必须失败,感觉有“强一致性“的味道。

总结

  • CountDownLanch是为计数器是设置一个值,当多次执行countdown后,计数器减为0的时候所有线程被唤醒,然后CountDownLanch失效,只能够使用一次。
  • CyclicBarrier是当count为0时同样唤醒全部线程,同时会重新设置count为parties,重新new一个generation来实现重复利用。

本文转载自微信公众号「java金融」,可以通过以下二维码关注。转载本文请联系java金融公众号。

 

责任编辑:武晓燕 来源: java金融
相关推荐

2021-03-11 00:05:55

Java高并发编程

2021-03-04 07:24:24

JavaSemaphore高并发

2020-08-27 08:17:05

缓存高并发系统

2021-02-26 13:08:27

Java高并发AQS

2024-04-29 09:06:46

线程初始化源码

2014-03-14 10:34:28

JavaJava并发

2019-09-16 09:23:34

高并发编程CountDownLaCyclicBarri

2020-12-03 11:15:21

CyclicBarri

2022-07-02 08:40:00

并发编程

2024-11-21 14:55:37

2020-09-21 06:53:41

NoSQL高并发面试

2022-11-27 08:12:11

RocketMQ源码工具类

2024-04-02 09:40:39

多线程Java原子性

2019-11-07 09:20:29

Java线程操作系统

2021-08-05 07:58:22

并发编程包Task

2011-07-05 14:42:46

java

2024-09-02 22:49:33

2016-11-28 09:08:43

java系统异步非阻塞

2021-02-14 18:26:25

高并发大对象代码

2023-11-01 16:01:00

数据类型Rust
点赞
收藏

51CTO技术栈公众号