JUC系列(七)| JUC三大常用工具类CountDownLatch、CyclicBarrier、Semaphore

简介: JUC系列(七)| JUC三大常用工具类CountDownLatch、CyclicBarrier、Semaphore

微信截图_20220525183707.png


多线程一直Java开发中的难点,也是面试中的常客,趁着还有时间,打算巩固一下JUC方面知识,我想机会随处可见,但始终都是留给有准备的人的,希望我们都能加油!!!

沉下去,再浮上来,我想我们会变的不一样的。 🍟我们:待别日相见时,都已有所成


关于封面:忽然之间,你忽略的,我忽略的所有细节当初的猜疑好奇,爱恨痴嗔全已走远


JUC系列

正在持续更新中...


JUC实际辅助类有五个,标题中三个最为常用。剩下未指明的分别为:Phaser、Exchanger。稍后会做简单讲解。


一、🎈CountDownLatch(减计数器)


1)概述:


CountDownLatch位于 java.util.concurrent包下。


CountDownLatch是一个同步辅助类,允许一个或多个线程等待,一直到其他线程执行的操作完成后再执行


CountDownLatch是通过一个计数器来实现的,计数器的初始值是线程的数量。每当有一个线程执行完毕后,然后通过 countDown 方法来让计数器的值-1,当计数器的值为0时,表示所有线程都执行完毕,然后继续执行 await 方法 之后的语句,即在锁上等待的线程就可以恢复工作了。


CountDownLatch中主要有两个方法:


  1. countDown


  • 递减锁存器的计数,如果计数达到零,则释放所有等待的线程


  • 如果当前计数大于零,则递减。 如果新计数为零,则为线程调度目的重新启用所有等待线程。


  • 如果当前计数为零,则什么也不会发生。


public void countDown() {
    sync.releaseShared(1);
}


  1. await


  • 使当前线程等待直到闩锁倒计时为零,除非线程被中断。


  • 如果当前计数为零,则此方法立即返回。即await 方法阻塞的线程会被唤醒,继续执行


  • 如果当前计数大于零,则当前线程出于线程调度目的而被禁用并处于休眠状态


public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}


2)案例:


举个生活中的小例子:


我们一寝室人去上课,得等到1、2、3、4、5、6、7、8个人都出来,才可以锁上寝室门吧。即当计数器值为0时,就可以执行await的方法啦。


编码步骤


  1. CountDownLatch countDownLatch = new CountDownLatch(8);


  1. countDownLatch.countDown(); 一个线程出来一个人,计数器就 -1


  1. countDownLatch.await(); 阻塞的等待计数器归零


  1. 执行后续步骤


我们用代码来模拟一下这个例子哈:


/**
 * @Author: crush
 * @Date: 2021-08-19 23:21
 * version 1.0
 */
public class CountDownLatchDemo1 {
    public static void main(String[] args) {
        // 初始值8 有八个人需要出寝室门
        CountDownLatch countDownLatch = new CountDownLatch(8);
        for (int i = 1; i <= 8; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "出去啦");
                // 出去一个人计数器就减1
                countDownLatch.countDown();
            }, String.valueOf(i)).start();
        }
        try {
            countDownLatch.await(); // 阻塞等待计数器归零
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 阻塞的操作 : 计数器  num++
        System.out.println(Thread.currentThread().getName() + "====寝室人都已经出来了,关门向教室冲!!!====");
    }
}


3)小结:


CountDownLatch使用给定的计数进行初始化。 由于调用了countDown方法,每次-1, await方法会一直阻塞到当前计数达到零,然后释放所有等待线程,并且任何后续的await调用都会立即返回。 这是一种一次性现象——计数无法重置。 如果您需要重置计数的版本,请考虑使用CyclicBarrier


CountDownLatch一个有用属性是它不需要调用countDown线程在继续之前等待计数达到零,它只是阻止任何线程通过await,直到所有线程都可以通过


二、🎀CyclicBarrier(加法计数器)


2.1、概述:


CyclicBarrier 看英文单词就可以看出大概就是循环阻塞的意思。所以还常称为循环栅栏。


CyclicBarrier 主要方法有:


public class CyclicBarrier {
    private int dowait(boolean timed, long nanos); // 供await方法调用 判断是否达到条件 可以往下执行吗
    //创建一个新的CyclicBarrier,它将在给定数量的参与方(线程)等待时触发,每执行一次CyclicBarrier就累加1,达到了parties,就会触发barrierAction的执行
    public CyclicBarrier(int parties, Runnable barrierAction) ;
    //创建一个新的CyclicBarrier ,参数就是目标障碍数,它将在给定数量的参与方(线程)等待时触发,每次执行 CyclicBarrier 一次障碍数会加一,如果达到了目标障碍数,才会执行 cyclicBarrier.await()之后的语句
    public CyclicBarrier(int parties) 
  //返回触发此障碍所需的参与方数量。
    public int getParties()
    //等待,直到所有各方都在此屏障上调用了await 。
  // 如果当前线程不是最后一个到达的线程,那么它会出于线程调度目的而被禁用并处于休眠状态.直到所有线程都调用了或者被中断亦或者发生异常中断退出
    public int await()
    // 基本同上 多了个等待时间 等待时间内所有线程没有完成,将会抛出一个超时异常
    public int await(long timeout, TimeUnit unit)
    //将障碍重置为其初始状态。 
    public void reset()
}


public CyclicBarrier(int parties):的构造方法第一个参数是目标障碍数,每次执行 CyclicBarrier 一次障碍数会加一,如果达到了目标障碍数,才会执行 cyclicBarrier.await()之后 的语句。可以将 CyclicBarrier 理解为加 1 操作。


public CyclicBarrier(int parties, Runnable barrierAction) :的构造方法第一个参数是目标障碍数,每次执行 CyclicBarrier 一次障碍数会加一,如果达到了目标障碍数,就会执行我们传入的Runnable;


2.2、案例:


我想大家多少玩过王者荣耀吧,里面不是有个钻石夺宝吗,抽201次必得荣耀水晶,这次让我们用代码来模拟一下吧。


编程步骤


  1. 创建CyclicBarrier对象
    CyclicBarrier cyclicBarrier = new CyclicBarrier(count, new MyRunnable());


  1. 编写业务代码


  1. cyclicBarrier.await(); //在线程里面等待阻塞,累加1,达到最大值count时,触发我们传入进去MyRunnable执行。


/**
 * @Author: crush
 * @Date: 2021-08-20 13:18
 * version 1.0
 */
public class CyclicBarrierDemo1 {
    public static void main(String[] args) {
        // 第一个参数:目标障碍数  第二个参数:一个Runnable任务,当达到目标障碍数时,就会执行我们传入的Runnable
        // 当我们抽了201次的时候,就会执行这个任务。
        CyclicBarrier cyclicBarrier = new CyclicBarrier(201,()->{
            System.out.println("恭喜你,已经抽奖201次,幸运值已满,下次抽奖必中荣耀水晶!!!");
        });
        for (int i=1;i<=201;i++){
            final int count=i;
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"抽奖一次");
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
    }
    // 这行代码是重置计数
    cyclicBarrier.reset();
    // 这里是我又加了 一次循环, 可以看到最后结果中输出了两次 "恭喜你"
    for (int i=1;i<=201;i++){
        final int count=i;
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"抽奖一次");
            try {
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        },String.valueOf(i)).start();
    }
}


2.3、小结:


CyclicBarrier和CountDownLatch其实非常相似,CyclicBarrier表示加法,CountDownLatch表示减法


区别还是有的:


  1. CyclicBarrier只能够唤醒一个任务,CountDownLatch可以唤起多个任务。


  1. CyclicBarrier可以重置,重新使用,但是CountDownLatch的值等于0时,就不可重复用了。


三、🩰Semaphore( 信号灯)


📍、概述:


Semaphore:信号量通常用于限制可以访问某些(物理或逻辑)资源的线程数


使用场景


限制资源,如抢位置、限流等。


🎃、案例:


【例子】:


不知道大家有没有过在网吧抢电脑打游戏的那种经历,小时候,平常便宜点的网吧都比较小,而且也比较少,特别多的人去,去晚了的人就只有站在那里看,等别人下机才能上网。


这次的例子就是:网吧有十台高配置打游戏的电脑,有20个小伙伴想要上网。


我们用代码来模拟一下:


编程步骤


  1. 创建信号灯
    Semaphore semaphore = new Semaphore(10); // 5个位置


  1. 等待获取信号灯
    semaphore.acquire();//等待获取许可证


  1. 业务代码


  1. 释放信号
    semaphore.release();//释放资源,女朋友来找了,下机下机,陪女朋友去了,那么就要释放这台电脑啦


/**
 * @Author: crush
 * @Date: 2021-08-20 14:03
 * version 1.0
 */
public class SemaphoreDemo1 {
    public static void main(String[] args) {
        // 10台电脑
        Semaphore semaphore = new Semaphore(10);
        // 20 个小伙伴想要上网
        for (int i = 1; i <= 20; i++) {
            new Thread(() -> {
                try {
                    //等待获取许可证
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + "抢到了电脑");
                    //抢到的小伙伴,迅速就开打啦 这里就模拟个时间哈,
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    //打完几把游戏的小伙伴 女朋友来找了 溜啦溜啦 希望大家都有人陪伴
                    System.out.println("女朋友来找,"+Thread.currentThread().getName() + "离开了");
                    semaphore.release();//释放资源,离开了就要把电脑让给别人啦。
                }
            }, String.valueOf(i)).start();
        }
    }
}


🎠、小结:


  1. 在获得一个项目之前,每个线程必须从信号量中获得一个许可,以保证一个项目可供使用。 当线程完成该项目时,它会返回到池中,并且将许可返回给信号量,允许另一个线程获取该项目。 请注意,调用acquire时不会持有同步锁,因为这会阻止项目返回到池中。 信号量封装了限制访问池所需的同步,与维护池本身一致性所需的任何同步分开。


  1. 初始化为 1 的信号量,并且使用时最多只有一个许可可用,可以用作互斥锁。 这通常被称为二进制信号量,因为它只有两种状态:一种许可可用,或零许可可用。 以这种方式使用时,二进制信号量具有属性(与许多java.util.concurrent.locks.Lock实现不同),即“锁”可以由所有者以外的线程释放(因为信号量没有所有权的概念)。 这在某些特定上下文中很有用,例如死锁恢复。


  1. 此类的构造函数可以选择接受公平参数。 当设置为 false 时,此类不保证线程获取许可的顺序。 当公平性设置为真时,信号量保证调用任何acquire方法的线程被选择以按照它们对这些方法的调用的处理顺序(先进先出;FIFO)获得许可。


  1. 通常,用于控制资源访问的信号量应初始化为公平的,以确保没有线程因访问资源而饿死。 当使用信号量进行其他类型的同步控制时,非公平排序的吞吐量优势通常超过公平性考虑。


  1. 内存一致性影响:在调用“释放”方法(如release()之前线程中的操作发生在另一个线程中成功的“获取”方法(如acquire()之后的操作之前。


四、🏏简单讲述 | Phaser & Exchanger


4.1、Phaser


Phaser一种可重用的同步屏障,功能上类似于CyclicBarrier和CountDownLatch,但使用上更为灵活。非常适用于在多线程环境下同步协调分阶段计算任务(Fork/Join框架中的子任务之间需同步时,优先使用Phaser)


//默认的构造方法,初始化注册的线程数量为0,可以动态注册
Phaser();
//指定了线程数量的构造方法
Phaser(int parties);
//添加一个注册者  向此移相器添加一个新的未到达方。 如果正在进行对onAdvance调用,则此方法可能会在返回之前等待其完成。
register();
//添加指定数量的注册者  将给定数量的新未到达方添加到此移相器(移相器就是Phaser)。
bulkRegister(int parties);
// 到达屏障点直接执行 无需等待其他人到达。
arrive();
//到达屏障点后,也必须等待其他所有注册者到达这个屏障点才能继续下一步
arriveAndAwaitAdvance();
//到达屏障点,把自己注销了,不用等待其他的注册者到达
arriveAndDeregister();
//多个线程达到注册点之后,会回调这个方法,可以做一些逻辑的补充
onAdvance(int phase, int registeredParties);


package com.crush.juc05;
import java.util.concurrent.Phaser;
public class PhaserDemo {
    private static Phaser phaser = new MyPhaser();
    //自定义一个移相器来自定义输出
    static class MyPhaser extends Phaser {
        /**
         * @deprecated 在即将到来的阶段提前时执行操作并控制终止的可覆盖方法。 此方法在推进此移相器的一方到达时调用(当所有其他等待方处于休眠状态时)。
         *             如果此方法返回true ,则此移相器将在提前时设置为最终终止状态,并且对isTerminated后续调用将返回 true。
         * @param phase 进入此方法的当前阶段号,在此移相器前进之前
         * @param registeredParties 当前注册方的数量
         * @return
         */
        @Override
        protected boolean onAdvance(int phase, int registeredParties) {
            if (phase == 0) {
                System.out.println("所有人都到达了网吧,准备开始开黑!!!");
                return false;
            } else if (phase == 1) {
                System.out.println("大家都同意,一起去次烧烤咯!!!");
                return false;
            } else if (phase == 2) {
                System.out.println("大家一起回寝室!!!");
                return true;
            }
            return true;
        }
    }
    //构建一个线程任务
    static class DoSomeThing implements Runnable {
        @Override
        public void run() {
            /**
             * 向此移相器添加一个新的未到达方
             */
            phaser.register();
            System.out.println(Thread.currentThread().getName() + "从家里出发,准备去学校后街上网开黑!!!");
            phaser.arriveAndAwaitAdvance();
            System.out.println(Thread.currentThread().getName() + "上着上着饿了,说去次烧烤吗?");
            phaser.arriveAndAwaitAdvance();
            System.out.println(Thread.currentThread().getName() + "烧烤次完了");
            phaser.arriveAndAwaitAdvance();
        }
    }
    public static void main(String[] args) throws Exception {
        DoSomeThing thing = new DoSomeThing();
        new Thread(thing, "小明").start();
        new Thread(thing, "小王").start();
        new Thread(thing, "小李").start();
    }
}
/**
 * 小李从家里出发,准备去学校后街上网开黑!!!
 * 小王从家里出发,准备去学校后街上网开黑!!!
 * 小明从家里出发,准备去学校后街上网开黑!!!
 * 所有人都到达了网吧,准备开始开黑!!!
 * 小李上着上着饿了,说去次烧烤吗?
 * 小明上着上着饿了,说去次烧烤吗?
 * 小王上着上着饿了,说去次烧烤吗?
 * 大家都同意,一起去次烧烤咯!!!
 * 小明烧烤次完了
 * 小李烧烤次完了
 * 小王烧烤次完了
 * 大家一起回寝室!!!
 */


注意:这里只是做了简单的一个使用,更深入的了解,我暂时也没有,想要研究可以去查一查。


4.2、Exchanger


Exchanger允许两个线程在某个汇合点交换对象,在某些管道设计时比较有用


Exchanger提供了一个同步点,在这个同步点,一对线程可以交换数据。每个线程通过exchange()方法的入口提供数据给他的伙伴线程,并接收他的伙伴线程提供的数据并返回。


当两个线程通过Exchanger交换了对象,这个交换对于两个线程来说都是安全的。


Exchanger可以认为是 SynchronousQueue 的双向形式,在运用到遗传算法和管道设计的应用中比较有用。


这个的使用我在Dubbo中的总体架构图中看到了它的身影


QQ截图20220525184211.png


五、🎡自言自语


最近又开始了JUC的学习,感觉Java内容真的很多,但是为了能够走的更远,还是觉得应该需要打牢一下基础。


最近在持续更新中,如果你觉得对你有所帮助,也感兴趣的话,关注我吧,让我们一起学习,一起讨论吧。


你好,我是博主宁在春,Java学习路上的一颗小小的种子,也希望有一天能扎根长成苍天大树。


希望与君共勉😁待我们,别时相见时,都已有所成


希望再遇见参考:JUC并发编程(八)-JUC常用辅助类


目录
相关文章
|
传感器 5G UED
5G 标准化进程|带你读《5G空口特性与关键技术》之二
从 2016 年起,3GPP 启动了 R14 研究项,目标是在 2020 年实现 5G 的商业化部署。为此,3GPP 采取了按阶段定义规范的方式。第一阶段目标是R15,旨在完成规范 5G 的有限功能。第二阶段是 R16,旨在完成规范 IMT-2020 所定义的所有功能,将于 2019 年年底到 2020 年完成。
5G 标准化进程|带你读《5G空口特性与关键技术》之二
|
Java Unix Linux
Maven 3.6.3 的下载、安装、配置、检测(详细讲解)
Maven 3.6.3 的下载、安装、配置、检测(详细讲解)
12824 0
Maven 3.6.3 的下载、安装、配置、检测(详细讲解)
|
数据可视化 大数据 定位技术
I+关系网络分析发布,提供完整的可视化分析和关系引擎功能
I+关系网络分析是以OLP模型为核心,面向业务快速建模,为开发者和终端用户提供大数据关系计算引擎(含API服务)和可视化交互分析能力,面向安防、关税、银行、保险、互联网等提供的产品化方案。目前,I+关系网络分析已在阿里巴巴、蚂蚁金服集团内广泛应用于反欺诈、反作弊、反洗钱等风控业务。
4710 0
|
12月前
|
Java API 容器
JAVA并发编程系列(10)Condition条件队列-并发协作者
本文通过一线大厂面试真题,模拟消费者-生产者的场景,通过简洁的代码演示,帮助读者快速理解并复用。文章还详细解释了Condition与Object.wait()、notify()的区别,并探讨了Condition的核心原理及其实现机制。
|
运维 监控 Devops
DevOps实践:从理论到落地的旅程
在软件开发和运维日益融合的今天,DevOps已不仅仅是一个流行词汇。它代表了一种文化和实践的转变,旨在打破部门间的壁垒,加速产品从构思到市场的流程。本文将带你了解DevOps的核心理念,并通过实际案例展示如何将这些理念应用到日常工作中,实现高效协作和持续改进。无论你是DevOps新手还是资深专家,这篇文章都将为你提供新的视角和实用的技巧。
|
设计模式 Java 数据处理
【Java并发编程系列8】多线程实战
Java多线程的学习,也有大半个月了,从开始学习Java多线程时,就给自己定了一个小目标,希望能写一个多线程的Demo,今天主要是兑现这个小目标。
1096 0
|
弹性计算 大数据 测试技术
阿里云2核8G云服务器价格多少钱?2024年阿里云2核8G云服务器性能测评
2024年阿里云2核8G云服务器的价格有两种说法。一种是2核8G配置,年付价格为877.32元。另一种是作为通用算力型u1实例,2核8G配置的价格为955.58元一年。关于阿里云2核8G云服务器的性能测评,该服务器配备了8GB的内存和2核的CPU,具有较高的计算能力和处理速度,能够满足大部分中小型网站和应用的需求。同时,服务器还提供了1M-3M的固定带宽,确保了网络连接的稳定性和快速性。ESSD Entry盘20G起的存储空间也能够满足一般用户的需求。总体来说,阿里云2核8G云服务器在性能和价格方面都有不错的表现,适合中小型企业和个人开发者使用。用户可以根据自己的实际需求选择合适的配置和促销
432 0
|
应用服务中间件 PHP 数据库
【搭建私人图床】使用LightPicture开源搭建图片管理系统并远程访问
现在的手机越来越先进,功能也越来越多,而手机的摄像功能也愈发强大,所拍摄的照片越来越清晰,但也让数码照片的体积暴涨。对于像笔者这样经常拍照的人来说,手机容量经常告警,因此笔者将家里的电脑改造成能随时上传下载和访问的图片服务器。今天,笔者就为大家展示,如何使用Cpolar内网穿透与Lightpicture组合,将个人电脑改造成能随时上传、下载或访问,并能生成外链的图床服务器。
|
消息中间件 资源调度 Java
【JUC基础】01. 初步认识JUC
前段时间,有朋友跟我说,能否写一些关于JUC的教程文章。本来呢,JUC也有在我的专栏计划之内,只是一直都还没空轮到他,那么既然有这样的一个契机,那就把JUC计划提前吧。那么今天就重点来初步认识一下什么是JUC,以及一些基本的JUC相关基础知识。
568 0
【JUC基础】01. 初步认识JUC
|
数据挖掘
ArcGIS:如何进行栅格数据的拼接和裁剪、坡度坡向的提取、地形透视图的建立、等高线的提取、剖面图的创建?
ArcGIS:如何进行栅格数据的拼接和裁剪、坡度坡向的提取、地形透视图的建立、等高线的提取、剖面图的创建?
705 0