多线程系列(十五) -常用并发工具类详解

一、摘要

在前几篇文章中,我们讲到了线程、线程池、BlockingQueue 等核心组件,其实 JDK 给开发者还提供了比synchronized更加高级的线程同步组件,比如 CountDownLatch、CyclicBarrier、Semaphore、Exchanger 等并发工具类。

下面我们一起来了解一下这些常用的并发工具类!

二、常用并发工具类

2.1、CountDownLatch

CountDownLatch是 JDK5 之后加入的一种并发流程控制工具类,它允许一个或多个线程一直等待,直到其他线程运行完成后再执行。

它的工作原理主要是通过一个计数器来实现,初始化的时候需要指定线程的数量;每当一个线程完成了自己的任务,计数器的值就相应得减 1;当计数器到达 0 时,表示所有的线程都已经执行完毕,处于等待的线程就可以恢复继续执行任务。

根据CountDownLatch的工作原理,它的应用场景一般可以划分为两种:

  • 场景一:某个线程需要在其他 n 个线程执行完毕后,再继续执行
  • 场景二:多个工作线程等待某个线程的命令,同时执行同一个任务

下面我们先来看下两个简单的示例。

示例1:某个线程等待 n 个工作线程

比如某项任务,先采用多线程去执行,最后需要在主线程中进行汇总处理,这个时候CountDownLatch就可以发挥作用了,具体应用如下!

public class CountDownLatchTest {public static void main(String[] args) throws InterruptedException {// 采用 10 个工作线程去执行任务final int threadCount = 10;CountDownLatch countDownLatch = new CountDownLatch(threadCount);for (int i = 0; i < threadCount; i++) {new Thread(new Runnable() {@Overridepublic void run() {// 执行具体任务System.out.println("thread name:" +  Thread.currentThread().getName() + ",执行完毕!");// 计数器减 1countDownLatch.countDown();}}).start();}// 阻塞等待 10 个工作线程执行完毕countDownLatch.await();System.out.println("所有任务线程已执行完毕,准备进行结果汇总");}
}

运行结果如下:

thread name:Thread-0,执行完毕!
thread name:Thread-2,执行完毕!
thread name:Thread-1,执行完毕!
thread name:Thread-3,执行完毕!
thread name:Thread-4,执行完毕!
thread name:Thread-5,执行完毕!
thread name:Thread-6,执行完毕!
thread name:Thread-7,执行完毕!
thread name:Thread-8,执行完毕!
thread name:Thread-9,执行完毕!
所有任务线程执行完毕,准备进行结果汇总
示例2:n 个工作线程等待某个线程

比如田径赛跑,10 个同学准备开跑,但是需要等工作人员发出枪声才允许开跑,使用CountDownLatch可以实现这一功能,具体应用如下!

public class CountDownLatchTest {public static void main(String[] args) throws InterruptedException {// 使用一个计数器CountDownLatch countDownLatch = new CountDownLatch(1);final int threadCount = 10;// 采用 10 个工作线程去执行任务for (int i = 0; i < threadCount; i++) {new Thread(new Runnable() {@Overridepublic void run() {try {// 阻塞等待计数器为 0countDownLatch.await();} catch (InterruptedException e) {e.printStackTrace();}// 发起某个服务请求,省略System.out.println("thread name:" +  Thread.currentThread().getName() + ",开始执行!");}}).start();}Thread.sleep(1000);System.out.println("thread name:" +  Thread.currentThread().getName() + " 准备开始!");// 将计数器减 1,运行完成后为 0countDownLatch.countDown();}
}

运行结果如下:

thread name:main 准备开始!
thread name:Thread-0,开始执行!
thread name:Thread-1,开始执行!
thread name:Thread-2,开始执行!
thread name:Thread-3,开始执行!
thread name:Thread-5,开始执行!
thread name:Thread-6,开始执行!
thread name:Thread-8,开始执行!
thread name:Thread-7,开始执行!
thread name:Thread-4,开始执行!
thread name:Thread-9,开始执行!

从上面的示例可以很清晰的看到,CountDownLatch类似于一个倒计数器,当计数器为 0 的时候,调用await()方法的线程会被解除等待状态,然后继续执行。

CountDownLatch类的主要方法,有以下几个:

  • public CountDownLatch(int count):核心构造方法,初始化的时候需要指定线程数
  • countDown():每调用一次,计数器值 -1,直到 count 被减为 0,表示所有线程全部执行完毕
  • await():等待计数器变为 0,即等待所有异步线程执行完毕,否则一直阻塞
  • await(long timeout, TimeUnit unit):支持指定时间内的等待,避免永久阻塞,await()的一个重载方法

从以上的分析可以得出,当计数器为 1 的时候,即由一个线程来通知其他线程,效果等同于对象的wait()notifyAll();当计时器大于 1 的时候,可以实现多个工作线程完成任务后通知一个或者多个等待线程继续工作,CountDownLatch可以看成是一种进阶版的等待/通知机制,在实际中应用比较多见。

2.2、CyclicBarrier

CyclicBarrier从字面上很容易理解,表示可循环使用的屏障,它真正的作用是让一组线程到达一个屏障时被阻塞,直到满足要求的线程数都到达屏障时,屏障才会解除,此时所有被屏障阻塞的线程就可以继续执行。

下面我们还是先看一个简单的示例,以便于更好的理解这个工具类。

public class CyclicBarrierTest {public static void main(String[] args) {// 设定参与线程的个数为 5int threadCount = 5;CyclicBarrier cyclicBarrier = new CyclicBarrier(threadCount, new Runnable() {@Overridepublic void run() {System.out.println("所有的线程都已经准备就绪...");}});for (int i = 0; i < threadCount; i++) {new Thread(new Runnable() {@Overridepublic void run() {System.out.println("thread name:" +  Thread.currentThread().getName() + ",已达到屏障!");try {cyclicBarrier.await();} catch (Exception e) {e.printStackTrace();}System.out.println("thread name:" +  Thread.currentThread().getName() + ",阻塞解除,继续执行!");}}).start();}}
}

输出结果:

thread name:Thread-0,已达到屏障!
thread name:Thread-1,已达到屏障!
thread name:Thread-2,已达到屏障!
thread name:Thread-3,已达到屏障!
thread name:Thread-4,已达到屏障!
所有的线程都已经准备就绪...
thread name:Thread-4,阻塞解除,继续执行!
thread name:Thread-0,阻塞解除,继续执行!
thread name:Thread-3,阻塞解除,继续执行!
thread name:Thread-1,阻塞解除,继续执行!
thread name:Thread-2,阻塞解除,继续执行!

从上面的示例可以很清晰的看到,CyclicBarrier中设定的线程数相当于一个屏障,当所有的线程数达到时,此时屏障就会解除,线程继续执行剩下的逻辑。

CyclicBarrier类的主要方法,有以下几个:

  • public CyclicBarrier(int parties):构造方法,parties参数表示参与线程的个数
  • public CyclicBarrier(int parties, Runnable barrierAction):核心构造方法,barrierAction参数表示线程到达屏障时的回调方法
  • public void await():核心方法,每个线程调用await()方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞,直到屏障解除,继续执行剩下的逻辑

从以上的示例中,可以看到CyclicBarrierCountDownLatch有很多的相似之处,都能够实现线程之间的等待,但是它们的侧重点不同:

  • CountDownLatch一般用于一个或多个线程,等待其他的线程执行完任务后再执行
  • CyclicBarrier一般用于一组线程等待至某个状态,当状态解除之后,这一组线程再继续执行
  • CyclicBarrier中的计数器可以反复使用,而CountDownLatch用完之后只能重新初始化
2.3、Semaphore

Semaphore通常我们把它称之为信号计数器,它可以保证同一时刻最多有 N 个线程能访问某个资源,比如同一时刻最多允许 10 个用户访问某个服务,同一时刻最多创建 100 个数据库连接等等。

Semaphore可以用于控制并发的线程数,实际应用场景非常的广,比如流量控制、服务限流等等。

下面我们看一个简单的示例。

public class SemaphoreTest {public static void main(String[] args) {SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");// 同一时刻仅允许最多3个线程获取许可final Semaphore semaphore = new Semaphore(3);// 初始化 5 个线程生成for (int i = 0; i < 5; i++) {new Thread(new Runnable() {@Overridepublic void run() {try {// 如果超过了许可数量,其他线程将在此等待semaphore.acquire();System.out.println(format.format(new Date()) +  " thread name:" +  Thread.currentThread().getName() + " 获取许可,开始执行任务");// 假设执行某项任务的耗时Thread.sleep(2000);} catch (Exception e) {e.printStackTrace();} finally {// 使用完后释放许可semaphore.release();}}}).start();}}
}

输出结果:

2023-11-22 17:32:01 thread name:Thread-0 获取许可,开始执行任务
2023-11-22 17:32:01 thread name:Thread-1 获取许可,开始执行任务
2023-11-22 17:32:01 thread name:Thread-2 获取许可,开始执行任务
2023-11-22 17:32:03 thread name:Thread-4 获取许可,开始执行任务
2023-11-22 17:32:03 thread name:Thread-3 获取许可,开始执行任务

从上面的示例可以很清晰的看到,同一时刻前 3 个线程获得了许可优先执行, 2 秒过后许可被释放,剩下的 2 个线程获取释放的许可继续执行。

Semaphore类的主要方法,有以下几个:

  • public Semaphore(int permits):构造方法,permits参数表示同一时间能访问某个资源的线程数量
  • acquire():获取一个许可,在获取到许可之前或者被其他线程调用中断之前,线程将一直处于阻塞状态
  • tryAcquire(long timeout, TimeUnit unit):表示在指定时间内尝试获取一个许可,如果获取成功,返回true;反之false
  • release():释放一个许可,同时唤醒一个获取许可不成功的阻塞线程。

通过permits参数的设定,可以实现限制多个线程同时访问服务的效果,当permits参数为 1 的时候,表示同一时刻只有一个线程能访问服务,相当于一个互斥锁,效果等同于synchronized

使用Semaphore的时候,通常需要先调用acquire()或者tryAcquire()获取许可,然后通过try ... finally模块在finally中释放许可。

例如如下方式,尝试在 3 秒内获取许可,如果没有获取就退出,防止程序一直阻塞。

// 尝试 3 秒内获取许可
if(semaphore.tryAcquire(3, TimeUnit.SECONDS)){try {// ...业务逻辑}  finally {// 释放许可semaphore.release();}
}
2.4、Exchanger

Exchanger从字面上很容易理解表示交换,它主要用途在两个线程之间进行数据交换,注意也只能在两个线程之间进行数据交换。

Exchanger提供了一个exchange()同步交换方法,当两个线程调用exchange()方法时,无论调用时间先后,会互相等待线程到达exchange()方法同步点,此时两个线程进行交换数据,将本线程产出数据传递给对方。

简单的示例如下。

public class ExchangerTest {public static void main(String[] args) {// 交换同步器Exchanger<String> exchanger = new Exchanger<>();// 线程1new Thread(new Runnable() {@Overridepublic void run() {try {String value = "A";System.out.println("thread name:" +  Thread.currentThread().getName() + " 原数据:" + value);String newValue = exchanger.exchange(value);System.out.println("thread name:" +  Thread.currentThread().getName() + " 交换后的数据:" + newValue);} catch (InterruptedException e) {e.printStackTrace();}}}).start();// 线程2new Thread(new Runnable() {@Overridepublic void run() {try {String value = "B";System.out.println("thread name:" +  Thread.currentThread().getName() + " 原数据:" + value);String newValue = exchanger.exchange(value);System.out.println("thread name:" +  Thread.currentThread().getName() + " 交换后的数据:" + newValue);} catch (InterruptedException e) {e.printStackTrace();}}}).start();}
}

输出结果:

thread name:Thread-0 原数据:A
thread name:Thread-1 原数据:B
thread name:Thread-0 交换后的数据:B
thread name:Thread-1 交换后的数据:A

从上面的示例可以很清晰的看到,当线程Thread-0Thread-1都到达了exchange()方法的同步点时,进行了数据交换。

Exchanger类的主要方法,有以下几个:

  • exchange(V x):等待另一个线程到达此交换点,然后将给定的对象传送给该线程,并接收该线程的对象,除非当前线程被中断,否则一直阻塞等待
  • exchange(V x, long timeout, TimeUnit unit):表示在指定的时间内等待另一个线程到达此交换点,如果超时会自动退出并抛超时异常

如果多个线程调用exchange()方法,数据交换可能会出现混乱,因此实际上Exchanger应用并不多见。

三、小结

本文主要围绕 Java 多线程中常见的并发工具类进行了简单的用例介绍,这些工具类都可以实现线程同步的效果,底层原理实现主要是基于 AQS 队列式同步器来实现,关于 AQS 我们会在后期的文章中再次介绍。

本文篇幅稍有所长,内容难免有所遗漏,欢迎大家留言指出!

四、参考

1.https://www.cnblogs.com/xrq730/p/4869671.html

2.https://zhuanlan.zhihu.com/p/97055716

五、写到最后

最近无意间获得一份阿里大佬写的技术笔记,内容涵盖 Spring、Spring Boot/Cloud、Dubbo、JVM、集合、多线程、JPA、MyBatis、MySQL 等技术知识。需要的小伙伴可以点击如下链接获取,资源地址:技术资料笔记。

不会有人刷到这里还想白嫖吧?点赞对我真的非常重要!在线求赞。加个关注我会非常感激!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/723612.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

OJ_空闲块

题干 C实现 /** 输入样例&#xff1a; 12 1024 2048 8192 512 16384 1024 32768 8192 65536 8192 77824 1024 80896 3072 86016 1024 91136 5120 99328 512 104448 1024 112640 3072 1024 2560 10240 512 1024 6400 512 -1 输出样例&#xff1a; 104448 1024 112640 3072 1024…

字节后端实习 一面凉经

心脏和字节永远都在跳动 深圳还有没有大厂招后端日常实习生啊&#xff0c;求捞&#xff5e;&#xff08;boss小公司也不理我&#xff09; 很纠结要不要干脆直接面暑期实习&#xff0c;又怕因为没有后端实习经历&#xff0c;面不到大厂实习。死锁了

SpringMVC-请求与响应(附Servlet相关接口替换方案)

1.请求 1.请求参数 SpringMVC将传递的参数封装到处理器方法的形参中&#xff0c;达到快速访问参数的目的 1.普通类型参数传参 page.jsp <% page contentType"text/html;charsetUTF-8" language"java" %> <html> <body> <h1>请…

从零学习Linux操作系统 第三十一部分 ansible常用模块介绍

一、ansible运行模块的两种方式 Ad-Hoc方式 ##利用ansible命令直接完成管理&#xff0c;主要用于临时命令使用场景 playbook方式 ##ansible脚本&#xff0c;主要用于大型项目场景&#xff0c;需要前期的规划&#xff0c;相当于shell当中的脚本 二、如何查看模块帮助 ansible…

基于Java springboot+VUE+redis实现的前后端分类版网上商城项目

基于Java springbootVUEredis实现的前后端分类版网上商城项目 博主介绍&#xff1a;多年java开发经验&#xff0c;专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 央顺技术团队 Java毕设项目精品实战案例《1000套》 欢迎点赞 收藏 ⭐留言…

DNS服务与管理

1. 规划节点 部署主从节点DNS服务的节点规划 IP主机名节点192.168.100.10master主DNS服务器192.168.100.20slave从DNS服务器 2. 基础准备 使用VMWare Workstation软件安装CentOS 7.2操作系统&#xff0c;镜像使用提供的 CentOS-7-x86_64-DVD-1511.iso&#xff0c;最小化Cen…

mysql从旧表 取出部分列并保存到新表几种方式介绍

在MySQL中&#xff0c;从旧表取出部分列并保存到新表有多种方式&#xff0c;主要包括以下几种&#xff1a; 1. 使用INSERT INTO ... SELECT语句&#xff1a; 这是最常用的方法。通过SELECT语句从旧表中选择需要的数据&#xff0c;然后使用INSERT INTO语句将数据…

shell 脚本 if-else判断 和流程控制 (基本语法|基础命令)

CSDN 成就一亿技术人&#xff01; 作者主页&#xff1a;点击&#xff01; Shell编程专栏&#xff1a;点击&#xff01; CSDN 成就一亿技术人 前言———— shell脚本中的if-else功能对于shell程序员来说是一笔重要的财富。当您需要根据预定义条件执行一组语句时&#xff0c…

MySQL 缓存策略

MySQL 缓存方案用来干什么 ? 缓存用户定义的热点数据&#xff0c;用户直接从缓存中获取热点数据&#xff0c;降低数据的读写压力。场景分析 内存访问速度是磁盘访问速度的 10 万倍。读的需求远远大于写的需求MySQL 自身缓冲层跟业务无关。MySQL 作为项目主要数据库&#xff0…

Cookie 探秘:了解 Web 浏览器中的小甜饼

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

C++ 之LeetCode刷题记录(三十八)

&#x1f604;&#x1f60a;&#x1f606;&#x1f603;&#x1f604;&#x1f60a;&#x1f606;&#x1f603; 开始cpp刷题之旅。 目标&#xff1a;执行用时击败90%以上使用 C 的用户。 18. 四数之和 给你一个由 n 个整数组成的数组 nums &#xff0c;和一个目标值 target…

CRM是什么?SaaS是什么?CRM和SaaS有什么关系?

CRM是什么&#xff1f;SaaS是什么&#xff1f;CRM和SaaS有什么关系&#xff1f; 接下来&#xff0c;我们就来好好唠唠CRM和SaaS。 下文提到的一款典型SaaS CRM模版先放在这儿了&#xff0c;有需要的可以自取——https://www.jiandaoyun.com 先来波名词解释吧 CRM是什么&#…

基于springboot的场地预约小程序的设计与实现(程序+数据库+文档)

** &#x1f345;点赞收藏关注 → 私信领取本源代码、数据库&#x1f345; 本人在Java毕业设计领域有多年的经验&#xff0c;陆续会更新更多优质的Java实战项目&#xff0c;希望你能有所收获&#xff0c;少走一些弯路。&#x1f345;关注我不迷路&#x1f345;** 目录 一、研…

《操作系统真相还原》读书笔记二:环境搭建 xshell连接virtualbox

修改 sshd_config 使用 vi /etc/ssh/sshd_config命令进入sshd服务配置&#xff0c;键盘输入i进行编辑&#xff0c;将监听端口、监听地址前的 # 号去除&#xff0c;开启允许远程登录&#xff0c;开启使用用户名密码来作为连接验证。修改完成&#xff0c;按一下Esc&#xff0c;输…

网络原理初识(1)

目录 一、网络发展史 1、独立模式 2、网络互联 3、局域网LAN 局域网组建的方式 1、基于网线直连 2、基于集线器组建 3、基于交换机组建 4、基于交换机和路由器组建 4、广域网WAN 二、网络通信基础 1、IP地址 2、端口号 3、认识协议 4、五元组 一、网络发展史 1、独立模式 …

Jmeter事务控制器聚合报告

Jmeter 事务控制器。 在Jmeter中&#xff0c;默认一个取样器就是一个事务事务控制器控制其子集取样器&#xff0c;合并为一个事务 添加&#xff1a;逻辑控制器/Logic Controller -> 事务控制器/Transaction Controller TPS: 服务器每秒处理的事务数在事务控制器下添加多个…

牛客网 华为机试 进制转换

本题是要将十六进制的字符串转换成十进制。看到题目第一眼就想到用map进行十六进制和十进制的映射。 然后我们需要注意&#xff0c;字符串前面会有0X&#xff0c;这只是一个标识十六进制的标识符&#xff0c;没有具体数字意义&#xff0c;我们在转换的时候&#xff0c;需要把它…

【视频转码】基于RK3588的视频转码探索

传统的视频转码服务基本都是基于X86下CPU、GPU转码&#xff0c;对硬件性能、功耗、成本来说都比较高。从技术角度来说现有视频转码技术有&#xff1a; 视频编码转变&#xff1a; 1. H.264 > H.265 保持视频分辨率、清晰度不变情况下&#xff0c;更改视频压缩方式&#xff0…

2024Java面试题知识点总结,一名毕业三年的女程序媛面试头条经验

程序员&#xff1a;给多少工资&#xff0c;干多少事 我们不是经常会看到一个关于西游记的“悖论”吗&#xff1a; 为什么孙悟空初期大闹天宫的时候那么厉害&#xff1f;因为他自己当老板&#xff0c;打一群天庭的打工仔。 为什么取经路上又变得不行了&#xff1f;作为一个打工…

牛客网 华为机试 取近似值

本题是要实现四舍五入。我们采用float的数据类型&#xff0c;因为这样数据精度更高。然后我们可以把得到的数据0.5&#xff0c;然后再转换成int数据类型&#xff0c;因为转换成int数据类型的时候是向下取整的&#xff0c;比如4.9转换成int就是4&#xff0c;4.2转换成int也是4。…