zoukankan      html  css  js  c++  java
  • CyclicBarrier:人齐了,老司机就可以发车了!

    上一篇咱讲了 CountDownLatch 可以解决多个线程同步的问题,相比于 join 来说它的应用范围更广,不仅可以应用在线程上,还可以应用在线程池上。然而 CountDownLatch 却是一次性的计数器,以王者农药来说,咱们不可能一场团战就决定比赛的输赢,所以在某些场景下,咱们是需要重复使用某个等待功能的,这就是我们今天要介绍的另一个主角——CyclicBarrier。

    CyclicBarrier

    CyclicBarrier 翻译为中文是循环(Cyclic)栅栏(Barrier)的意思,它的大概含义是实现一个可循环利用的屏障。
    image.png
    CyclicBarrier 作用是让一组线程相互等待,当达到一个共同点时,所有之前等待的线程再继续执行,且 CyclicBarrier 功能可重复使用。

    20181218144511688.gif

    举个栗子

    比如磊哥要坐班车回老家,因为中途不允许上、下乘客,所以营运的公司为了收益最大化,就会等人满之后再发车。像这种等人坐满就发一班车的场景,就是 CyclicBarrier 所擅长的,因为它可以重复使用(不像 CountDownLatch 那样只能用一次)。
    image.png

    CyclicBarrier VS CountDownLatch

    CountDownLatch:一个或者多个线程,等待另外 N 个线程完成某个事情之后才能执行。

    CountDownLatch 就像玩王者农药开局的加载一样,所有人要等待其他人都加载 100% 之后才能开始游戏。

    image.png

    CyclicBrrier:N 个线程相互等待,直到有足够数量的线程都到达屏障点之后,之前等待的线程就可以继续执行了。

    CyclicBrrier 就像老司机开车一样,如果车上还有空余的座位,那么所有人都得等着,直到座位被坐满之后,老司机才会发车。
    老司机发车.gif

    CyclicBarrier使用

    import java.util.Date;
    import java.util.Random;
    import java.util.concurrent.*;
    
    public class CyclicBarrierExample {
        public static void main(String[] args) {
            // 创建 CyclicBarrier
            final CyclicBarrier cyclicBarrier = new CyclicBarrier(2, new Runnable() {
                @Override
                public void run() {
                    System.out.println("人满了,准备发车:" + new Date());
                }
            });
            
            // 线程调用的任务
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    // 生成随机数 1-3
                    int randomNumber = new Random().nextInt(3) + 1;
                    // 进入任务
                    System.out.println(String.format("我是:%s 再走:%d 秒就到车站了,现在时间:%s",
                            Thread.currentThread().getName(), randomNumber, new Date()));
                    try {
                        // 模拟执行
                        TimeUnit.SECONDS.sleep(randomNumber);
                        // 调用 CyclicBarrier
                        cyclicBarrier.await();
                        // 任务执行
                        System.out.println(String.format("线程:%s 上车,时间:%s",
                                Thread.currentThread().getName(), new Date()));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                }
            };
    
            // 创建线程池
            ExecutorService threadPool = Executors.newFixedThreadPool(10);
            // 执行任务 1
            threadPool.submit(runnable);
            // 执行任务 2
            threadPool.submit(runnable);
            // 执行任务 3
            threadPool.submit(runnable);
            // 执行任务 4
            threadPool.submit(runnable);
    
            // 等待所有任务执行完终止线程池
            threadPool.shutdown();
        }
    }
    

    以上代码执行结果如下:
    image.png
    从上述结果可以看出:当 CyclicBarrier 的计数器设置为 2 时,线程 2 和 线程 3 都到屏障点之后,老司机才会发第一波车,再 2s 之后,线程 1 和线程 4 也同时进入了屏障点,这时候老司机又可以再发一波车了。

    实现原理

    我们先来看下 CyclicBarrier 的类图:
    image.png
    由上图可知 CyclicBarrier 是基于独占锁 ReentrantLock 实现的,其底层也是基于 AQS 的。

    在 CyclicBarrier 类的内部有一个计数器 count,当 count 不为 0 时,每个线程在到达屏障点会先调用 await 方法将自己阻塞,此时计数器会减 1,直到计数器减为 0 的时候,所有因调用 await 方法而被阻塞的线程就会被唤醒继续执行。当 count 计数器变成 0 之后,就会进入下一轮阻塞,此时 parties(parties 是在 new CyclicBarrier(parties) 时设置的值)会将它的值赋值给 count 从而实现复用。

    常用方法

    CyclicBarrier(parties):初始化相互等待的线程数量的构造方法。

    CyclicBarrier(parties,Runnable barrierAction):初始化相互等待的线程数量以及屏障线程的构造方法,当 CyclicBarrier 的计数器变为 0 时,会执行 barrierAction 构造方法。

    getParties():获取 CyclicBarrier 打开屏障的线程数量,也称为方数。

    getNumberWaiting():获取正在CyclicBarrier上等待的线程数量。

    await():在 CyclicBarrier 上进行阻塞等待,直到发生以下情形之一:
    在 CyclicBarrier 上等待的线程数量达到 parties,则所有线程被释放,继续执行;

    • 当前线程被中断,则抛出 InterruptedException 异常,并停止等待,继续执行;
    • 其他等待的线程被中断,则当前线程抛出 BrokenBarrierException 异常,并停止等待,继续执行;
    • 其他等待的线程超时,则当前线程抛出 BrokenBarrierException 异常,并停止等待,继续执行;
    • 其他线程调用 CyclicBarrier.reset() 方法,则当前线程抛出 BrokenBarrierException 异常,并停止等待,继续执行。

    await(timeout,TimeUnit):在CyclicBarrier上进行限时的阻塞等待,直到发生以下情形之一:

    • 在 CyclicBarrier 上等待的线程数量达到 parties,则所有线程被释放,继续执行;
    • 当前线程被中断,则抛出 InterruptedException 异常,并停止等待,继续执行;
    • 当前线程等待超时,则抛出 TimeoutException 异常,并停止等待,继续执行;
    • 其他等待的线程被中断,则当前线程抛出 BrokenBarrierException 异常,并停止等待,继续执行;
    • 其他等待的线程超时,则当前线程抛出 BrokenBarrierException 异常,并停止等待,继续执行;
    • 其他线程调用 CyclicBarrier.reset() 方法,则当前线程抛出 BrokenBarrierException 异常,并停止等待,继续执行。

    isBroken():获取是否破损标志位 broken 的值,此值有以下几种情况:

    • CyclicBarrier 初始化时,broken=false,表示屏障未破损;
    • 如果正在等待的线程被中断,则 broken=true,表示屏障破损;
    • 如果正在等待的线程超时,则 broken=true,表示屏障破损;
    • 如果有线程调用 CyclicBarrier.reset() 方法,则 broken=false,表示屏障回到未破损状态。

    reset():使得CyclicBarrier回归初始状态,直观来看它做了两件事:

    • 如果有正在等待的线程,则会抛出 BrokenBarrierException 异常,且这些线程停止等待,继续执行。
    • 将是否破损标志位 broken 置为 false。

    总结

    CyclicBrrier 是通过独占锁 ReentrantLock 实现计数器的原子性更新的,CyclicBrrier 最常用的是 await() 方法,使用此方法会将计数器 -1,并判断当前的计数器是否为 0,如果不为 0 就会阻塞等待,并计时器为 0 之后,才能继续执行剩余任务。CyclicBrrier 相比于 CountDownLatch 来说,它的优势在于可以重复使用。

    参考 & 鸣谢

    blog.csdn.net/qq_39241239/article/details/87030142
    blog.csdn.net/zzg1229059735/article/details/61191679
    www.cnblogs.com/yaochunhui/p/13494689.html

    关注公众号:「Java中文社群」查看更多精彩内容。

    关注下面二维码,订阅更多精彩内容。
    微信打赏
    关注公众号(加好友):

  • 相关阅读:
    Azure PowerShell (7) 使用CSV文件批量设置Virtual Machine Endpoint
    Windows Azure Cloud Service (39) 如何将现有Web应用迁移到Azure PaaS平台
    Azure China (7) 使用WebMetrix将Web Site发布至Azure China
    Microsoft Azure News(4) Azure新D系列虚拟机上线
    Windows Azure Cloud Service (38) 微软IaaS与PaaS比较
    Windows Azure Cloud Service (37) 浅谈Cloud Service
    Azure PowerShell (6) 设置单个Virtual Machine Endpoint
    Azure PowerShell (5) 使用Azure PowerShell创建简单的Azure虚拟机和Linux虚拟机
    功能代码(1)---通过Jquery来处理复选框
    案例1.用Ajax实现用户名的校验
  • 原文地址:https://www.cnblogs.com/vipstone/p/14635809.html
Copyright © 2011-2022 走看看