CountDownLatch、CyclicBarrier实战场景分析(附代码)

概述

   一直一来,对于JUC包中提供的工具、锁、集合等都在学习,奈何一直没有学习完😉😉,人懒没办法,一般都是用到啥学啥。我喜欢结合实际场景,尤其是并发这种东西,本身就比较抽象。对于CountDownLatch和CyclicBarrier这两个并发编程的辅助工具,在实战中有幸使用CountDownLatch来解决实际生产问题,至于CyclicBarrier一直未曾使用过,最近刚好有点时间,我们一起探讨一下这两个工具的实战使用场景⭐️⭐️⭐️⭐️⭐️⭐️

CountDownLatch概述

首先我们看一下官方给出该工具的定义👇👇👇
   A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.
A CountDownLatch is initialized with a given count. The await methods block until the current count reaches zero due to invocations of the countDown() method, after which all waiting threads are released and any subsequent invocations of await return immediately. This is a one-shot phenomenon – the count cannot be reset. If you need a version that resets the count, consider using a CyclicBarrier.
   A CountDownLatch is a versatile synchronization tool and can be used for a number of purposes. A CountDownLatch initialized with a count of one serves as a simple on/off latch, or gate: all threads invoking await wait at the gate until it is opened by a thread invoking countDown(). A CountDownLatch initialized to N can be used to make one thread wait until N threads have completed some action, or some action has been completed N times.
A useful property of a CountDownLatch is that it doesn’t require that threads calling countDown wait for the count to reach zero before proceeding, it simply prevents any thread from proceeding past an await until all threads could pass.
简单来说:
      1.是一个同步工具,允许一个或多个线程等待直到其他线程均工作完毕。
      2.初始化需要给定约定好的count,各个线程调用countDown()方法将count值为0时,await()阻塞的线程们可以继续执行,这里我们姑且称它为减法计数器
      3.该工具不能重置,仅能使用一次。

CountDownLatch实战之前置任务处理

有一次呢,狗哥准备和两个女朋友约会,由于人多也不好意思去外面玩,那么就把约会地点选到了家里,狗哥就分别去接了露西和戴安娜然后回到了家中,到家后露西就给狗哥做了个马杀鸡,同时呢戴安娜给狗哥去放好了洗澡水。一番甜蜜的约会后,狗哥幸福的进入了梦想。
我们分析一下这段业务,首先呢狗哥是个单线程,而两个女朋友分别是两个工作线程,业务逻辑很简单,狗哥先串行的接到两个女朋友到达家中,然后两个女朋友在同一时刻分别给狗哥来了个按摩,另外一个给狗哥放洗澡水,最后省略后续约会过程,狗哥进入梦想,我们看看代码怎么实现。

package com.cn.scott.test;

import com.google.common.collect.Maps;

import java.util.HashMap;
import java.util.concurrent.CountDownLatch;

public class CountDownLatchTest {


    static class Gfriend implements Runnable{

        private final CountDownLatch startSignal;
        private final CountDownLatch doneSignal;

        private String doSomeThing;

        public Gfriend (CountDownLatch start ,CountDownLatch done,String thing){
            startSignal = start;
            doneSignal = done;
            doSomeThing = thing;
        }

        @Override
        public void run() {

            try {
                startSignal.await();
                doSomeThing(doSomeThing);
                doneSignal.countDown();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

        private void doSomeThing(String thing){
            System.out.println(thing);
        }
    }


    private static void beforeTodo(){
        System.out.println("狗哥分别接露西和戴安娜回家约会");
    }

    private static void afterTodo(){
        System.out.println("狗哥约会结束,打着香甜的呼噜进入了梦想Zzzzzz.....");
    }


    public static void main(String[] args) throws InterruptedException {
        HashMap<Integer, String> map = Maps.newHashMap();
        map.put(1,"露西在给狗哥按摩");
        map.put(2,"戴安娜在给狗哥放洗澡水");

        final CountDownLatch startSignal = new CountDownLatch(1);
        final CountDownLatch doneSignal = new CountDownLatch(2);

        for (int i = 1; i <= 2; ++i) {
            new Thread(new Gfriend(startSignal, doneSignal,map.get(i))).start();
        }
        // 其中主线程是狗哥 两个工作线程分别是两个女朋友
        // 我们定义了两个减法计数器
        // startSignal 初始化count为1 用来控制工作线程在主线程工作后进行执行
        // doneSignal 初始化为2 用来控制两个工作线程执行后 主线程的收尾工作
        beforeTodo();
        startSignal.countDown();
        doneSignal.await();
        afterTodo();

    }
}

我们看下执行结果:
      狗哥带着女朋友回家约会
      露西在给狗哥按摩
      戴安娜在给狗哥放洗澡水
      狗哥约会结束,打着香甜的呼噜进入了梦想Zzzzzz…

CountDownLatch实战之后置任务处理

狗哥上次约会之后呢,又策划了一次郊游,但是由于自己比较懒,所以让两个女朋友分别准备东西,露西负责准备便当,戴安娜负责准备帐篷,狗哥需要等待他们准备完成后开车带他们一起去郊游。
这段业务很简单,狗哥的单线程需要等待两个女朋友的多线程的任务完毕后执行。

package com.cn.scott.test;

import com.google.common.collect.Maps;

import java.util.HashMap;
import java.util.concurrent.CountDownLatch;

public class CountDownLatchTest2 {

    static class Gfriend implements Runnable {

        private final CountDownLatch countDownLatch;

        private String doSomeThing;

        public Gfriend(CountDownLatch start, String thing) {
            countDownLatch = start;
            doSomeThing = thing;
        }

        @Override
        public void run() {
            doSomeThing(doSomeThing);
            countDownLatch.countDown();
        }

        private void doSomeThing(String thing) {
            System.out.println(thing);
        }
    }

    private static void afterPickUp() {
        System.out.println("狗哥等到了露西和戴安娜,拉着他们一起去郊游");
    }

    public static void main(String[] args) throws InterruptedException {
        HashMap<Integer, String> map = Maps.newHashMap();
        map.put(1, "露西准备了郊游用的便当,并且出发");
        map.put(2, "戴安娜准备了郊游用的3人帐篷");

        final CountDownLatch startSignal = new CountDownLatch(2);

        for (int i = 1; i <= 2; ++i) {
            new Thread(new Gfriend(startSignal, map.get(i))).start();
        }
        // 主线程是狗哥 工作线程分别是两个女朋友
        // startSignal 初始化2 用来控制狗哥任务执行进程需要在女朋友们的准备工作都完毕时进行唤醒
        startSignal.await();
        afterPickUp();

    }
}

执行结果如下:
      露西准备了郊游用的便当,并且出发
      戴安娜准备了郊游用的3人帐篷
      狗哥等到了露西和戴安娜,拉着他们一起去郊游

CountDownLatch实战之最优接口逻辑

下面我们讲狗哥的第三次约会,狗哥最近出了露西、戴安娜,又新找了一个女朋友格瑞斯,有一天狗哥急用钱,就给自己的女朋友群里发了消息说,我需要一千块,你们谁有给我送过来,第一个人送过来之后,其他人送来我就不要了,越快越好。
这个逻辑是不是有点意思,实际生产中你们是否遇到过类似的场景,比如让你开发一个翻译文字的接口,接口接受到参数之后,需要调用百度翻译、有道翻译、谷歌翻译三个接口,哪个接口最快,就返回前端翻译结果。先不要看代码,想想怎么做。

package com.cn.scott.test;

import com.google.common.collect.Maps;
import java.util.HashMap;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class CountDownLatchTest3 {

    private static AtomicInteger atomicInteger = new AtomicInteger(0);

    static class Gfriend implements Runnable {

        private final CountDownLatch countDownLatch;

        private String doSomeThing;
        private Integer sleepTime;

        public Gfriend(CountDownLatch start, String thing, Integer count) {
            countDownLatch = start;
            doSomeThing = thing;
            sleepTime = count * 1000;
        }

        private void doSomeThing(String thing) {
            System.out.println(thing);
        }

        @Override
        public void run() {

            try {
                Thread.sleep(sleepTime);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            if (atomicInteger.get() != 0) {
                System.out.println(doSomeThing + ",但是狗哥已经拿到其他女朋友给他的钱,所以不需要了");
                return;
            }
            atomicInteger.set(sleepTime / 1000);
            countDownLatch.countDown();
        }
    }

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        HashMap<Integer, String> map = Maps.newHashMap();
        map.put(1, "露西直接通过微信支付给了狗哥1w块");
        map.put(2, "戴安娜从妈妈那里借了钱后准备给狗哥送去");
        map.put(3, "格瑞斯去银行取钱后准备给狗哥送去");

        final CountDownLatch startSignal = new CountDownLatch(1);
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        for (int i = 1; i <= 3; ++i) {
            executorService.execute(new Gfriend(startSignal, map.get(i), i));
        }
        // 主线程是狗哥 工作线程分别是三个女朋友
        // startSignal 初始化1 用来计数 当三个任务中有一个完成时 则继续执行主线程
        // AtomicInteger 用于记录哪个女朋友最先拿到钱,并且阻止其他女朋友的工作线程继续工作。
        startSignal.await();
        System.out.println(map.get(atomicInteger.get()) + ",狗哥收到钱之后给了女朋友一个么么哒");
        shutdownAndAwaitTermination(executorService);

    }

    static void shutdownAndAwaitTermination(ExecutorService pool) {
        pool.shutdown();
        try {
            if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {

            }
        } catch (InterruptedException ie) {
            pool.shutdownNow();

            Thread.currentThread().interrupt();
        }
    }

}

执行结果如下:
      露西直接通过微信支付给了狗哥1w块,狗哥收到钱之后给了女朋友一个么么哒
      戴安娜从妈妈那里借了钱后准备给狗哥送去,但是狗哥已经拿到其他女朋友给他的钱,所以不需要了
      格瑞斯去银行取钱后准备给狗哥送去,但是狗哥已经拿到其他女朋友给他的钱,所以不需要了

CyclicBarrier概述

同样我们看一下官方给出该工具的定义👇👇👇
   A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point. CyclicBarriers are useful in programs involving a fixed sized party of threads that must occasionally wait for each other. The barrier is called cyclic because it can be re-used after the waiting threads are released.
   A CyclicBarrier supports an optional Runnable command that is run once per barrier point, after the last thread in the party arrives, but before any threads are released. This barrier action is useful for updating shared-state before any of the parties continue.
简单来说:
      1.是一个同步工具,他是让一组线程互相等待,达到一个共同的屏障点,每个线程到达屏障点后对count值进行累,直到count满足初始化给定的值,这里我们姑且称它为加法计数器
      2.该工具允许重复使用,提供了reset方法。
      3.该工具提供了屏障点执行动作,可以在所有线程到达屏障点后执行。

CyclicBarrier、CountDownLatch混合实战

我们继续来说狗哥和三个女朋友的故事,经过多次约会,狗哥日久生情,决定考虑长远发展,找到三个女朋友说,以后发了工资都由我来保管,每月十号把钱都交给我,我来统一分配,给你们没人分配一定的零花钱,你们可以自由支配,其他钱存进银行,我们开始攒钱。然后每月25号我们来计算一下你们每人结余。
这个需求有那么一丢丢的复杂了,同样也是多线程协作,涉及到了任务汇总以及线程协作工具的重复使用。大家可以想想怎么实现。
笔者的实现思路:
      1.月初多个女朋友并发的去给狗哥上交工资
      2.等所有女朋友交齐了钱后 狗哥根据总收入统一分配零用钱,这时候女朋友们可以去自行消费
      3.月底到了狗哥继续收女朋友剩余的钱

package com.cn.scott.test;

import com.google.common.collect.Maps;
import org.springframework.util.StringUtils;

import java.util.HashMap;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.atomic.AtomicInteger;

public class CyclicBarrierTest {
    private static final HashMap<Integer, Integer> salaryMap = Maps.newHashMap();

    private static final HashMap<Integer, Integer> leftMap = Maps.newHashMap();

    private static final HashMap<Integer, String> nameMap = Maps.newHashMap();

    private static final HashMap<Integer, String> afterMap = Maps.newHashMap();


    private static AtomicInteger sum = new AtomicInteger(0);

    static class Gfriend implements Runnable {

        private final CyclicBarrier cyclicBarrier;
        private final CountDownLatch countDownLatch;

        private Integer money;
        private String name;

        private String doSomeThing;

        public Gfriend(CyclicBarrier cyclicBarrier, CountDownLatch countDownLatch, String thing, Integer money, String name) {
            this.cyclicBarrier = cyclicBarrier;
            this.countDownLatch = countDownLatch;
            this.doSomeThing = thing;
            this.money = money;
            this.name = name;
        }

        private void doSomeThing(String thing) {
            if (!StringUtils.isEmpty(thing)) {
                System.out.println(thing);
            }
        }

        @Override
        public void run() {
            try {
                System.out.println(name + "给狗哥转账" + money + "元");
                sum.getAndAdd(money);
                cyclicBarrier.await();
                doSomeThing(doSomeThing);
                countDownLatch.countDown();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } catch (BrokenBarrierException e) {
                throw new RuntimeException(e);
            }

        }
    }

    public static void main(String[] args) throws InterruptedException {
        //这次业务分为两部分 
        //第一部分是月初多个女朋友并发的去交钱 等所有女朋友交齐了钱后 狗哥根据总收入统一分配零用钱,这时候女朋友们可以去自行消费
        //第二部分是在第一部分执行完之后,月底到了狗哥继续收女朋友剩余的钱
        //所以我们使用了CyclicBarrier 让多个女朋友线程之间互相等待,相当于一个减法计数器,当女朋友线程都交钱完毕,通过barrierAction进行收入汇总
        //当收入汇总完毕之后 女朋友们又能拿着狗哥给的钱自行消费
        //同时我们还需要CountDownLatch 来控制批次顺序
        //这个示例中可以清晰的卡出来 CyclicBarrier可以通过reset()方法重复使用 而CountDownLatch则需要重新构建
        CountDownLatch countDownLatch = new CountDownLatch(3);
        Runnable barrierAction = () -> {
            System.out.println("狗哥累计收到女朋友们上交的" + sum.get() + "元");
        };
        final CyclicBarrier cyclicBarrier = new CyclicBarrier(3, barrierAction);
        for (int i = 1; i <= 3; ++i) {
            new Thread(new Gfriend(cyclicBarrier, countDownLatch, afterMap.get(i), salaryMap.get(i), nameMap.get(i))).start();
        }
        countDownLatch.await();

        CountDownLatch countDownLatch2 = new CountDownLatch(3);
        System.out.println("-----月底到了,女朋友们要把手上剩余的钱继续交给狗哥----");
        cyclicBarrier.reset();
        sum.set(0);
        for (int i = 1; i <= 3; ++i) {
            new Thread(new Gfriend(cyclicBarrier, countDownLatch2, null, leftMap.get(i), nameMap.get(i))).start();
        }
        countDownLatch2.await();
        System.out.println("想到女朋友们都这么会过,狗哥留下了欣慰的眼泪....");
    }


    static {
        salaryMap.put(1, 6000);
        salaryMap.put(2, 7000);
        salaryMap.put(3, 8000);
        leftMap.put(1, 500);
        leftMap.put(2, 500);
        leftMap.put(3, 500);
        nameMap.put(1, "露西");
        nameMap.put(2, "戴安娜");
        nameMap.put(3, "格瑞斯");
        afterMap.put(1, "露西花钱买了超短裙");
        afterMap.put(2, "戴安娜花钱买了面膜");
        afterMap.put(3, "格瑞斯花钱买了SK2");
    }
}

执行结果如下:
      露西给狗哥转账6000元
      格瑞斯给狗哥转账8000元
      戴安娜给狗哥转账7000元
      狗哥累计收到女朋友们上交的21000元
      戴安娜花钱买了面膜
      露西花钱买了超短裙
      格瑞斯花钱买了SK2
      -----月底到了,女朋友们要把手上剩余的钱继续交给狗哥----
      露西给狗哥转账500元
      戴安娜给狗哥转账500元
      格瑞斯给狗哥转账500元
      狗哥累计收到女朋友们上交的1500元
      想到女朋友们都这么会过,狗哥留下了欣慰的眼泪…
可以看出代码达到了我们想要的结果✌️✌️✌️

小结

1.CountDownLatch是减法计数器,CyclicBarrier是加法计数器
2.CountDownLatch不可重复使用,CyclicBarrier可以通过重置进行重复使用
3.CyclicBarrier是线程之间互相等待,并且他提供了barrierAction进行任务后处理的拓展
4.使用场景也略有不同,但是灵活使用都能达到自己的目的,一千个程序员有一千种写法。
点赞收藏,富婆包养✋✋

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

代码大师麦克劳瑞

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值