可口的JAVA-并发控制之CyclicBarrier

625 阅读6分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。 本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

注:掘金潜水怪今日起开更 注:老后端不水图

前言

前文给大家介绍了CountDownLatch,本文给大家介绍一下姊妹篇CyclicBarrier。老规矩还是从介绍、方法、场景、原理四个角度方面开展学习,话不多说,马上开始。

一:介绍

官方介绍: 这是一种同步辅助,允许一组线程全部等待彼此到达公共屏障点。 CyclicBarriers在涉及固定大小的线程组的程序中很有用,必须偶尔互相等待。屏障被称为 Cyclic因为它可以在等待线程被释放后重新使用。CyclicBarrier 支持可选的 Runnable 命令 在每个屏障点运行一次,在parties中的最后一个线程 * 到达之后,但在任何线程被释放之前。这个cyclic对于在任何一方继续之前更新共享状态很有用。

翻译一下就是:

  • CyclicBarrier 有一个公共屏障点,所有线程在被释放前都等待在这里。
  • CyclicBarrier 是一个可以传入一个Runnable对象,在所有线程被释放后,先执行该Runnable线程。
  • CyclicBarrier 允许循环使用,这点跟CountDownLatch(只能用一次)不同。

    二:方法

    CyclicBarrier公开方法主要有:

    int getParties() //需要等待阻塞线程的个数
    
    int await() //等待,直到所有各方都在此屏障上调用了await (中断,reset等方法)
    
    boolean isBroken() //查询此屏障是否处于破坏状态
    
    void reset() //将障碍重置为其初始状态。如果任何一方正在 * 当前等待屏障,他们将返回 * {@link BrokenBarrierException}。
    
    int getNumberWaiting() //返回当前在障碍处等待的参与方数量
    

    我举一个简单的例子,辅助大家对CyclicBarrier有一个深刻的认识。 小猪参加比赛,等所有小猪都到准备好后开始比赛,最后得到胜者。 代码:

    /**
     * @function 单任务执行
     */
    @SneakyThrows
    static void singleTask(){
        CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
        System.out.println("猪子们都来比赛了");
        new Thread(new Pig(cyclicBarrier),"A猪").start();
        new Thread(new Pig(cyclicBarrier),"B猪").start();
        new Thread(new Pig(cyclicBarrier),"C猪").start();
        Thread.sleep(2000);
        System.out.println("全部结束比赛");
    }
    
    /**
     * 这是一群小猪猪
     */
    class Pig implements Runnable{
        private CyclicBarrier cyclicBarrier;
        public Pig(CyclicBarrier cyclicBarrier1){
            cyclicBarrier = cyclicBarrier1;
        }
        @Override
        public void run() {
            try {
                Thread.sleep((int) (Math.random() * 1000));
                // 猪猪到达门口
                System.out.println(Thread.currentThread().getName() + "到达出发地点");
                System.out.println("broken::" + cyclicBarrier.isBroken() + ",parties::" + cyclicBarrier.getParties() +
                        ",waiting::" + cyclicBarrier.getNumberWaiting());
                cyclicBarrier.await();
                System.out.println(Thread.currentThread().getName() + "结束比赛。。");
            }catch (BrokenBarrierException e){
                System.out.println(Thread.currentThread().getName() +"::"+e.getClass());
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
    

    根据代码,当所有小猪猪到达出发地点之后 (都执行了await方法),CyclicBarrier条件满足,开闸放猪,最后结束比赛。代码中还分别打印了 broken是否开闸的状态、parties所有条件数目、waiting当前达到条件数目。看运行结果:

    猪子们都来比赛了
    C猪到达出发地点
    broken::false,parties::3,waiting::0
    A猪到达出发地点
    broken::false,parties::3,waiting::1
    B猪到达出发地点
    broken::false,parties::3,waiting::2
    B猪结束比赛。。
    C猪结束比赛。。
    A猪结束比赛。。
    全部结束比赛
    

    三:场景

    CyclicBarrier跟CountDownLatch比较,增加了可循环使用的功能和自主重置条件功能,减少了countDown()功能,调用await()方法时会自动减一。可应用场景也更加强大,可以使用循环栅栏、栅栏条件手动重置等情况。

    情况一:普通栅栏

    可以参考上面小猪比赛的例子。

    情况二:循环栅栏

    由于小猪们比较多,场地有限,只能采取每组两个头猪的小组赛策略来进行比赛,但又不能针对每族比赛都修改一次代码(成本太高),便采用循环栅栏。 代码示例:

    /**
     * @function cyclicBarrier可以重复利用
     * 1 反复使用
     */
    @SneakyThrows
    static void mulitTask(){
        CyclicBarrier cyclicBarrier = new CyclicBarrier(2, new Runnable() {
            @Override
            public void run() {
                System.out.println("人员到齐,开始比赛");
            }
        });
        System.out.println("猪子们都来比赛了");
        new Thread(new Pig(cyclicBarrier),"A猪").start();
        new Thread(new Pig(cyclicBarrier),"B猪").start();
        Thread.sleep(2000);
        new Thread(new Pig(cyclicBarrier),"C猪").start();
        new Thread(new Pig(cyclicBarrier),"D猪").start();
        Thread.sleep(2000);
        new Thread(new Pig(cyclicBarrier),"E猪").start();
        new Thread(new Pig(cyclicBarrier),"F猪").start();
    }
    

    每组放出两头小猪进行比赛,依次进行。 运行结果:

    猪子们都来比赛了
    B猪到达出发地点
    broken::false,parties::2,waiting::0
    A猪到达出发地点
    broken::false,parties::2,waiting::1
    人员到齐,开始比赛
    A猪结束比赛。。
    B猪结束比赛。。
    C猪到达出发地点
    broken::false,parties::2,waiting::0
    D猪到达出发地点
    broken::false,parties::2,waiting::1
    人员到齐,开始比赛
    D猪结束比赛。。
    C猪结束比赛。。
    E猪到达出发地点
    broken::false,parties::2,waiting::0
    F猪到达出发地点
    broken::false,parties::2,waiting::1
    人员到齐,开始比赛
    F猪结束比赛。。
    E猪结束比赛。。
    

    分析结果,没两组进行一次比赛,CyclicBarrier进行了多次重复利用。

    场景三:手动重置栅栏条件

    小猪在比赛的过程当中,存在作弊抢跑等行为,为了遏制这一行为,有猪犯规后将被取消比赛资格,其他剩余猪猪继续比赛。 示例代码:

    /**
     * @function reset重置
     * 1.reset后正在等待的线程会抛出异常 BrokenBarrierException
     */
    @SneakyThrows
    static void resetTask(){
        CyclicBarrier cyclicBarrier = new CyclicBarrier(2, new Runnable() {
            @Override
            public void run() {
                System.out.println("通过cyclic后执行");
            }
        });
        System.out.println("猪子们都来比赛了");
        new Thread(new Pig(cyclicBarrier),"A猪").start();
        new Thread(new Pig2(cyclicBarrier),"有猪犯规,重置比赛").start();
        new Thread(new Pig(cyclicBarrier),"B猪").start();
        new Thread(new Pig(cyclicBarrier),"C猪").start();
        Thread.sleep(1000);
        System.out.println("结束比赛");
    }
    
    /**
     * 这是一群小猪猪
     */
    class Pig implements Runnable{
        private CyclicBarrier cyclicBarrier;
        public Pig(CyclicBarrier cyclicBarrier1){
            cyclicBarrier = cyclicBarrier1;
        }
        @Override
        public void run() {
            try {
                Thread.sleep((int) (Math.random() * 1000));
                // 猪猪到达门口
                System.out.println(Thread.currentThread().getName() + "到达出发地点");
                System.out.println("broken::" + cyclicBarrier.isBroken() + ",parties::" + cyclicBarrier.getParties() +
                        ",waiting::" + cyclicBarrier.getNumberWaiting());
                cyclicBarrier.await();
                System.out.println(Thread.currentThread().getName() + "结束比赛。。");
            }catch (BrokenBarrierException e){
                System.out.println(Thread.currentThread().getName() +"犯规,取消比赛资格::"+e.getClass());
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
    
    class Pig2 implements Runnable{
        private CyclicBarrier cyclicBarrier;
        public Pig2(CyclicBarrier cyclicBarrier1){
            cyclicBarrier = cyclicBarrier1;
        }
        @Override
        public void run() {
            try {
                Thread.sleep((int) (Math.random() * 1000));
                // 猪猪到达门口
                cyclicBarrier.reset();
                System.out.println(Thread.currentThread().getName() + "reset");
            }catch (Exception e){
                System.out.println(Thread.currentThread().getName() +"::"+e.getMessage());
            }
        }
    }
    

    查看运行结果:

    猪子们都来比赛了
    B猪到达出发地点
    broken::false,parties::2,waiting::0
    有猪犯规,重置比赛reset
    B猪犯规,取消比赛资格::class java.util.concurrent.BrokenBarrierException
    A猪到达出发地点
    broken::false,parties::2,waiting::0
    C猪到达出发地点
    broken::false,parties::2,waiting::1
    通过cyclic后执行
    C猪结束比赛。。
    A猪结束比赛。。
    结束比赛
    

    四:原理

    CyclicBarrier底层采用了 ReentrantLock 保持操作同步,基本原理如下:

  • 首先构建CyclicBarrier对象,初始化parties、count和Runnable(可选)参数。
  • 线程调用await方法时,Cyclic对象内部先检查计数器是否为0,如果是就执行Runable,接下来开始下一个new generation()。
  • 每一个generation的初始化,都会重置count值为parties,并重新 new Generation()实例。
  • 每次重置generation 首先要调用Condition的signalAll方法,唤醒所有正在栅栏前等待的线程。
  • 如果计数器>0,就调用线程Condition的await方法,使线程在栅栏前进行等待。
  • 由于所有方法都采用ReentrantLock锁住,所以都是同步操作,不存在多线程问题。

    欢迎大家点赞评论!!!!