JavaEE初阶-多线程进阶2
文章目录
- 前言
- 一、CAS
- 1.1 CAS的概念
- 1.2 原子类
- 1.3 CAS的ABA问题
- 二、JUC中常用类
- 2.1 Callable接口
- 2.2 ReentrantLock(可重入)
- 2.3 Semaphore信号量
- 2.4 CountDownLatch类
- 2.5 CopyOnWriteArrayList类
- 2.6 ConcurrentHashMap
前言
对于多线程进阶的部分,更多总结的就是面试常考,但是工作中开发中不常用到的知识。
一、CAS
1.1 CAS的概念
CAS就是compare and swap的首字母缩写,意味着比较和交换,这样的一条指令即可完成比较和交换这一套操作,也就是说这套操作是原子的。
我们可以将CAS的流程想象成一个方法。

这里的交换其实思想上更偏向于赋值,因为一般更关注于内存地址address中的内容而不关心寄存器reg2中的内容,所以就可以近似说这里的操作就是将reg2的值赋给了address地址。
CAS一般就是cpu中的一条指令,所以操作系统为了使用它完成这样的操作就需要去提供这样的CAS的api。然后JVM又对这样的api进行了封装,使得我们在java中也能够使用CAS操作了。但是实际上这样的CAS操作被封装到了“unsafe”包当中,就是提醒大家容易出错,不鼓励直接使用CAS。
1.2 原子类
Java当中也有一些类对CAS进行了进一步的封装,就比如说原子类。

如上图的AtomicInteger就相当于对int进行了封装,对于它的++或者–操作都是原子的,实例代码如下:
package thread;import java.util.concurrent.atomic.AtomicInteger;public class Demo41 {public static AtomicInteger count = new AtomicInteger(0);public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {for (int i = 0; i < 50000; i++) {//count++ 这里的对count的修改都是原子的count.getAndIncrement();//++count//count.incrementAndGet();//--count//count.decrementAndGet();//count--//count.getAndDecrement();//count+=10;//count.getAndAdd(10);}});Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {count.getAndIncrement();}});t.start();t1.start();t.join();t1.join();System.out.println(count);}
}
这里的多线程代码就是经典的两个线程两个循环来计算count值,因为这里的count使用到了原子类的方法,所以加一操作是原子性的,自然不存在线程安全的问题,也能够得到正确结果。
那么使用这种原子性操作的意义是什么呢?意义就在于效率,因为锁是一个很重量级的操作,如果操作没有原子性在多线程的情况下就要加锁,但是我们可以使用CAS从而不去使用锁,从而提高代码效率。这一套基于CAS不加锁实现线程安全代码的方式,也被称为“无锁编程”。但是CAS这种方法也就仅仅适用于少数场景。
1.3 CAS的ABA问题
属于CAS的一个重要注意事项,CAS的核心就是“比较-发现相等-交换”->发现相等即数据没发生任何改变,但是相等不等于没改变过。可能值经历了一个从A到B再到A的过程,这种情况在极端环境下会产生问题。

如上图取款操作,假如我们要取500,情急之下,我们多按了两次取款按钮,此时产生了两个线程来进行扣款操作,但是如果在此时别人给你转了500,那么就会出现问题了。

如图左边是t1线程,右边是t2线程,t2线程完成扣款五百之后,此时t3线程给账户又转了500,此时应该不成立的t1线程的判断又成立了,导致又完成一次扣款。上述的过程就是典型的ABA问题所造成的bug,是非常极端的情况。
如何去避免这样的问题呢?可以约定一个版本号,每次进行扣款或存款都更新版本号,如果版本号没有改变数据就一定没变过。

通过版本号约束就可以避免这里的ABA问题,避免多次扣款。即使t3线程仍然给账户汇了500,但是此时版本号已经是2了,所以t1线程的版本号对不上,方法内部的扣款操作无法完成,所以即使有两个线程去扣款,扣的款也只有500。
二、JUC中常用类
JUC是java.util.concurrent这个包的首字母,在这里介绍一下这个包当中的常用类。
2.1 Callable接口
我们都知道Runnable接口用来表示一个待执行的任务,Callable接口和Runnable也是相似的,他也是用来表示一个待执行的任务,但是Callable有返回值,表示这个线程执行结束要得到的结果是啥。
public class Demo42 {private static int count = 0;public static void main(String[] args) throws InterruptedException, ExecutionException {//使用Runnable来求出1~100的和Thread t = new Thread(new Runnable() {@Overridepublic void run() {int result = 0;for (int i = 1; i <= 100; i++) {result += i;}//需要用成员变量来接收值 主线程和t线程的耦合程度高 如果有多个这样的线程就不方便了count = result;}});
以上给出了一段代码,就是使用类变量count来得到线程结果,这样的代码等线程多了之后很不方便,代码不够优雅。Callable就是用来解决上述代码的问题的。接下来给出全部代码用于对比:
package thread;import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;public class Demo42 {private static int count = 0;public static void main(String[] args) throws InterruptedException, ExecutionException {//使用Runnable来求出1~100的和Thread t = new Thread(new Runnable() {@Overridepublic void run() {int result = 0;for (int i = 1; i <= 100; i++) {result += i;}//需要用成员变量来接收值 主线程和t线程的耦合程度高 如果有多个这样的线程就不方便了count = result;}});// t.start();// t.join();// System.out.println(count);// Callable和Runnable很相似 但是Runnable可以返回计算的值Callable<Integer> callable = new Callable<Integer>() {@Overridepublic Integer call() throws Exception {int result = 0;for (int i = 1; i <= 100; i++) {result += i;}return result;}};// futuretask这个类用来包装callable这个类 这样callable就可以直接放入线程FutureTask<Integer> futureTask = new FutureTask<>(callable);Thread t2 = new Thread(futureTask);t2.start();//从future获取线程启动通过callable计算得到的值t2.join();System.out.println(futureTask.get());}}
如以上代码,Callable接口需要使用FutureTask来包装,包装之后就将FutureTask对象放入线程,线程执行完成之后就可以通过FutureTask对象来得到线程执行的结果。
2.2 ReentrantLock(可重入)
在以前的JDK中,synchronized还没现在那么好用,那时ReentrantLock还是非常有市场的。但是随着版本的迭代,synchronized越来越强,基本上需要加锁的时候无脑使用synchronized大概率不会出问题。那么ReentrantLock现在还有什么价值?
(1)ReentrantLock实现了公平锁

这里代码中的参数写true就是公平锁,false就是非公平锁。
(2)ReentrantLock提供了tryLock操作,给加锁提供了更多的操作空间。
尝试加锁,如果该锁已经被获取了,那么就直接失败返回,不会继续等待。tryLock还有一个类似版本就是可以指定等待的时间,超时后返回。
(3)synchronized搭配wait以及notify的等待通知机制,ReentrantLock搭配Condition类完成等待通知。
Condition类比wait以及notify强一点。(多个线程wait,notify唤醒随机一个。Condition指定线程唤醒)
2.3 Semaphore信号量
信号量是一个非常简单的概念,就是一个计数器,描述了可用资源的数目。围绕信号量有两个操作,P操作,计数器减一,申请资源,V操作,计数器加一,释放资源。提出信号量的是荷兰人,PV是荷兰语的首字母,在英语中是acquire就是获取,以及release表示释放。代码示例如下:
package thread;import java.util.concurrent.Semaphore;public class Demo44 {public static void main(String[] args) throws InterruptedException {// 四个可用资源 P申请资源 V释放资源Semaphore semaphore = new Semaphore(4);semaphore.acquire(1);System.out.println("P操作");semaphore.acquire(1);System.out.println("P操作");semaphore.acquire(1);System.out.println("P操作");semaphore.acquire(1);System.out.println("P操作");// 此时信号量的四个资源已经被申请完了// 如果继续申请的话就会堵塞 因为要等别的线程释放信号量的资源semaphore.acquire(1);}}
以上代码信号量拥有四个单位的资源,然后通过acquire方法来申请资源,当资源被申请完并且没有资源释放时,再次申请资源就会阻塞。当设置信号量资源为一个单位,则信号量取值只能为1或者0,此时的信号量可以当成锁来使用。代码示例如下:
package thread;import java.util.concurrent.Semaphore;public class Demo45 {public static int count = 0;public static void main(String[] args) throws InterruptedException {//设置 1 0 信号量Semaphore semaphore = new Semaphore(1);Thread t1 = new Thread(() -> {try {for (int i = 0; i < 50000; i++) {semaphore.acquire(1);count++;semaphore.release();}} catch (InterruptedException e) {throw new RuntimeException(e);}});Thread t2 = new Thread(() -> {try {for (int i = 0; i < 50000; i++) {semaphore.acquire(1);count++;semaphore.release();}} catch (InterruptedException e) {throw new RuntimeException(e);}});t1.start();t2.start();t1.join();t2.join();System.out.println(count);}
}
以上代码其实还是多线程代码的经典例子,使用两个线程来计算累加值。当t1进行count加一的操作时,它已经申请了唯一的信号量资源,此时如果t2线程也想进行count加一就必须先执行申请信号量资源的操作,此时就会阻塞,只有当t1线程的count++执行结束之后释放资源,t2线程才能继续执行,这就实现了count++操作的原子性,从而避免线程安全问题。
2.4 CountDownLatch类
相对来说比较实用的工具类,当我们把一个任务分为多个时,就可以通过这个工具类来识别任务是否整体执行完毕了。代码示例如下:
package thread;import java.util.concurrent.CountDownLatch;public class Demo46 {public static void main(String[] args) throws InterruptedException {CountDownLatch latch = new CountDownLatch(10);for (int i = 0; i < 10; i++) {int temp = i;Thread t = new Thread(() -> {System.out.println("线程启动:" + temp);//当作任务try {Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("线程结束:" + temp);latch.countDown();});t.start();}//等待所有线程中的任务结束latch.await();System.out.println("所有线程结束。");}}
这段代码中我们给CountDownLatch类对象的参数为10,并且创建10个线程去执行任务,并且在每个线程中使用countDown方法,countDown方法相当于计数,当一个线程结束就会加一,latch.await()方法就会等待所有线程执行结束,当countDown方法累加的数等于初始化CountDownLatch对象的参数时await方法就会停止等待,整个代码就运行结束了。这里代码中CountdownLatch对象的参数和线程数相等,并且每个线程都放了countDown方法,所以所有线程运行结束await方法也就不等了。
2.5 CopyOnWriteArrayList类
ArrayList,LinkedList,Stack,Queue,HashMap…在多线程下使用集合类需要注意线程安全问题。Vector自带了synchronized,Stack继承自Vector所以也有synchronized,HashTable也是自带synchronized。但是需要注意一点,加锁不代表就是线程安全的,不加锁也不能确定线程就是一定不安全的,需要具体代码具体分析。
在我们使用未加锁的类时需要手动进行加锁,这样是比较麻烦的,标准库提供了一些其它的解决方案,如下图。

通过这样的操作,给ArrayList这些集合类套一层壳,就是给一些关键方法加上了synchronized,使得ArrayList达到Vector那样的效果。
CopyOnWriteArrayList类也是一种解决线程安全问题的方法。

如果当前有多个线程读列表上的数据,那么不需要做任何处理。如果某个线程对上面的数据进行修改,此时另一个线程进行读取,那么很可能会读到200 3这样的中间情况。CopyOnWriteArrayList这样的类就是一种写时拷贝,在你对列表进行修改时会开辟新空间在新空间上进行修改,你要读取数据那么就在旧空间进行读取,当修改完成后将新的列表的引用代替旧的引用,旧的空间就可以释放了。这样的过程没有任何的加锁和阻塞,也能保证线程读不到错误的数据。
这种方法的思想应用的很广,例如显卡渲染画面到显示器,显示的动态效果其实就是很多张图片,由于显卡渲染足够快这些图片就能融合在一起,看到动画效果。实际上就是写时拷贝,在显示上一个画面的时候,在背后的额外空间生成下一个画面,生成完毕了用下一个画面代替上一个画面。
2.6 ConcurrentHashMap
我们知道HashMap是线程不安全的,HashTable是带锁的,是否是线程安全的?事实上并不推荐使用这个,标准库提供了更好的替代也就是ConcurrentHashMap。
HashTable加锁就是简单粗暴的给每个方法加了synchronized,就相当于针对this加锁,只要针对HashTable上的元素进行操作,就都会涉及到锁冲突。
ConcurrentHashMap做出了以下优化:
(1)使用锁桶的方式来代替一把全局锁,有效降低冲突概率。

这一点很好理解,如果有两个线程针对两个不同的链表进行操作,那么它们之间是不会产生锁冲突的。本身两个线程修改的是不同的链表,也没涉及到“公共变量”,所以不涉及线程安全问题。这个提升是非常大的,因为一个哈希表上的桶非常多,桶之间发生冲突的概率非常小,并且synchronized我们前面的博客也讲过了,只要不发生冲突synchronized只是加了一个偏向锁,就类似一个标记,消耗非常小。
(2)对于哈希表的size即使你修改的不同链表/桶,但是你在多线程的情况下也会涉及到多个线程修改一个公共变量的问题,在ConcurrentHashMap中对于size的修改就是使用CAS这种具有原子性的语句来完成,这样不仅避免了加锁这种重量级的操作,也解决了线程安全的问题。
(3)针对扩容进行了特殊优化。
如果发现负载因子太大了,那么就需要扩容,然而扩容又是比较低效的操作,普通的HashMap要在一次put的过程中完成整个扩容过程,就会使得put操作非常卡。ConcurrentHashMap就会在扩容的时候整出另外的一份空间,每次进行哈希表的基本操作都会将一部分扩容之前空间的数据搬到新空间,不是一口气搬完而是分多次,在搬的过程中如果是插入操作就将新数据插入到新空间,删除操作,新旧空间都进行删除,查找操作,新旧空间都要查找。
另外值得一提的是,在java8之前ConcurrentHashMap是基于分段锁的形式进行实现的,就是引入多个锁对象,每个锁对象去管理若干个哈希桶。相比于HashTable这个方法是进化,但是还是不如直接锁桶,后面就把这个方法给废弃了。
相关文章:
JavaEE初阶-多线程进阶2
文章目录 前言一、CAS1.1 CAS的概念1.2 原子类1.3 CAS的ABA问题 二、JUC中常用类2.1 Callable接口2.2 ReentrantLock(可重入)2.3 Semaphore信号量2.4 CountDownLatch类2.5 CopyOnWriteArrayList类2.6 ConcurrentHashMap 前言 对于多线程进阶的部分&…...
B/S和C/S框架
一、B/S框架 B/S框架是指Browser/Server框架,即基于浏览器和服务器的应用程序开发框架。在B/S架构中,用户通过浏览器(Browser)访问服务器(Server)上的应用程序或网站,而无需在用户端安装额外的客…...
机器学习中常用的几种距离——欧式、余弦等
目录 一、欧式距离(L2距离)二、曼哈顿距离(L1距离)三、汉明距离四、余弦相似度 一、欧式距离(L2距离) (1)二维空间的距离公式(三维空间的在这个基础上类推)&…...
2024 Google I/O Android 相关内容汇总
2024 Google I/O Android 相关内容汇总 本次 Google I/O 的核心虽然是 AI ,但是 Android 也是作为主要议题出现, Android 部分可以简单分为产品和开发相关内容,接下来主要介绍这两部分的相关更新。 重点开始开发相关,内容不少 产…...
# 从浅入深 学习 SpringCloud 微服务架构(十八)
从浅入深 学习 SpringCloud 微服务架构(十八) 一、开源配置中心 Apollo:概述 1、开源配置中心 Apollo Apollo -A reliable configuration management system Apollo(阿波罗)是携程框架部门研发的分布式配置中心,能够集中化管理…...
在SQL Server中使用临时表与普通表的性能差异分析
在SQL Server中,临时表和普通表的性能确实存在差异,具体表现和影响因素如下: 临时表和普通表的区别 存储位置: 临时表:存储在tempdb数据库中,生命周期仅限于当前会话或批处理。当会话结束或批处理完成时&a…...
数据中台管理系统原型
数据中台是一个通用性的基础平台,适用于各类行业场景,数据中台包含多元数据汇聚、数据标准化、数据开发、数据共享、数据智能、数据资产管理等功能,助力企业数字化转型。 数据汇聚 数据汇聚是将不同系统、不同类型的多元源数据汇聚至目标数据…...
数据库练习
在数据库中创建一个表student,用于存储学生信息 CREATE TABLE student( id INT PRIMARY KEY, name VARCHAR(20) NOT NULL, grade FLOAT ); 1、向student表中添加一条新记录(记录中id字段的值为1,name字段的值为"monkey",…...
Rust学习笔记(上)
前言 笔记的内容主要参考与《Rust 程序设计语言》,一些也参考了《通过例子学 Rust》和《Rust语言圣经》。 Rust学习笔记分为上中下,其它两个地址在Rust学习笔记(中)和Rust学习笔记(下)。 编译与运行 Ru…...
【SRC实战】文件名回显导致反射型XSS,URL重定向
挖个洞先 https://mp.weixin.qq.com/s/hnrm-snkETuR-gqPOSnQXQ “ 以下漏洞均为实验靶场,如有雷同,纯属巧合 ” 01 — 漏洞证明 一、反射型XSS “ 文件名回显,能否触发XSS?” 1、灯塔扫到敏感文件,发现1.txt会在…...
mysql高版本导入低版本Unknown collation: utf8mb4_0900_ai_ci
MySQL数据库导入SQL报错 Unknown collation: ‘utf8mb4_0900_ai_ci‘ 错误原因:我本地的MySQL数据包版本为8.0的,而服务器上的MySQL版本为5.7,双方的版本不兼容,这样就导致我在本地写好的SQL无法在服务器上的MySQL上运行。 解决办…...
运筹说 第114期 | 其他排队模型简介
前面我们已经学习了一些排队模型,对排队系统有了基本认知,本期小编带大家继续来学习其他排队模型的内容。 一、有限源排队模型 顾客源为有限的这类排队问题的主要特征是顾客总数是有限的,如只有m个顾客。每个顾客来到系统中接受服务后仍回到…...
易基因: WGBS等揭示梨驯化和改良过程中DNA甲基化对果实成熟的作用机制 | 作物育种
大家好,这里是专注表观组学十余年,领跑多组学科研服务的易基因。 梨(Pyrus ssp.,蔷薇科杏仁核亚科)是世界上最重要的温带水果作物之一。与野生梨相比,栽培梨的果实在许多形态特征上表现出显著变化…...
数据分析(二)——导入外部数据,导入Excel数据,CSV文件,txt文件,HTML网页,数据抽取,DataFrame对象的loc属性与iloc属性
一.导入外部数据 1.导入.xIs或.xIsx文件 pd.read_ excel(io,sheet_ name,header) 1.1常用参数说明 ●io:表示.xIs或.xIsx文件路径或类文件对象 ●sheet name:表示工作表,取值如下表所示 ●header:默认值为0,取第一行的值为列名,数据为除列…...
如何让Linux系统崩溃?
如何使 Linux 系统崩溃 警告 下面的代码行是 Bash shell 的一个简短而甜蜜的 fork 炸弹。分叉炸弹之所以有效,是因为它能够产生无限数量的进程。最终,Linux无法处理所有这些,并且会崩溃。 fork 炸弹的一大优点是你不需要 root 权限即可执行它…...
C# 实现邮件推送功能
🏆作者:科技、互联网行业优质创作者 🏆专注领域:.Net技术、软件架构、人工智能、数字化转型、DeveloperSharp、微服务、工业互联网、智能制造 🏆欢迎关注我(Net数字智慧化基地),里面…...
企业微信私域营销:构建高效转化新引擎
在数字营销的大潮中,企业微信作为企业内部沟通协作的重要工具,也逐渐成为企业私域营销的重要战场。本文将带您深入了解企业微信私域营销的魅力,探讨如何构建高效转化的新引擎。 一、企业微信私域营销:为何重要? 企业微…...
电力物联网-(2)系统设计
电力物联网系统设计 前言 在此之前写过《电力物联网系统设计》开篇文章,上一篇文章主要的概述性的内容,发表之后总觉得对电力物联网系统设计这一方面还只是开了一个头,没有把相关的内容讲解清楚,于是经过一段时间的构思终于产出了…...
如何确保企业信息的真实性和可靠性,保障企业权益、降低合作风险
企业四要素核验:确保企业信息真实性的关键步骤 随着信息技术的飞速发展,企业信息的准确性和可靠性对于商业决策、合作伙伴选择以及风险管理等方面都显得尤为重要。为了确保企业信息的真实性和可靠性,企业四要素核验成为了一项关键步骤。本文…...
用python写一个自动生成android开机动画的工具
要创建一个自动生成Android开机动画的工具,你需要一些基本的知识,比如Python编程、图像处理和Android开机动画的格式。以下是一个简单的Python脚本示例,它可以生成一个基本的Android开机动画,具体效果可能需要更多的调整和优化。 …...
QMC5883L的驱动
简介 本篇文章的代码已经上传到了github上面,开源代码 作为一个电子罗盘模块,我们可以通过I2C从中获取偏航角yaw,相对于六轴陀螺仪的yaw,qmc5883l几乎不会零飘并且成本较低。 参考资料 QMC5883L磁场传感器驱动 QMC5883L磁力计…...
大型活动交通拥堵治理的视觉算法应用
大型活动下智慧交通的视觉分析应用 一、背景与挑战 大型活动(如演唱会、马拉松赛事、高考中考等)期间,城市交通面临瞬时人流车流激增、传统摄像头模糊、交通拥堵识别滞后等问题。以演唱会为例,暖城商圈曾因观众集中离场导致周边…...
测试markdown--肇兴
day1: 1、去程:7:04 --11:32高铁 高铁右转上售票大厅2楼,穿过候车厅下一楼,上大巴车 ¥10/人 **2、到达:**12点多到达寨子,买门票,美团/抖音:¥78人 3、中饭&a…...
CocosCreator 之 JavaScript/TypeScript和Java的相互交互
引擎版本: 3.8.1 语言: JavaScript/TypeScript、C、Java 环境:Window 参考:Java原生反射机制 您好,我是鹤九日! 回顾 在上篇文章中:CocosCreator Android项目接入UnityAds 广告SDK。 我们简单讲…...
Java入门学习详细版(一)
大家好,Java 学习是一个系统学习的过程,核心原则就是“理论 实践 坚持”,并且需循序渐进,不可过于着急,本篇文章推出的这份详细入门学习资料将带大家从零基础开始,逐步掌握 Java 的核心概念和编程技能。 …...
10-Oracle 23 ai Vector Search 概述和参数
一、Oracle AI Vector Search 概述 企业和个人都在尝试各种AI,使用客户端或是内部自己搭建集成大模型的终端,加速与大型语言模型(LLM)的结合,同时使用检索增强生成(Retrieval Augmented Generation &#…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...
LangChain知识库管理后端接口:数据库操作详解—— 构建本地知识库系统的基础《二》
这段 Python 代码是一个完整的 知识库数据库操作模块,用于对本地知识库系统中的知识库进行增删改查(CRUD)操作。它基于 SQLAlchemy ORM 框架 和一个自定义的装饰器 with_session 实现数据库会话管理。 📘 一、整体功能概述 该模块…...
Mysql8 忘记密码重置,以及问题解决
1.使用免密登录 找到配置MySQL文件,我的文件路径是/etc/mysql/my.cnf,有的人的是/etc/mysql/mysql.cnf 在里最后加入 skip-grant-tables重启MySQL服务 service mysql restartShutting down MySQL… SUCCESS! Starting MySQL… SUCCESS! 重启成功 2.登…...
STM32HAL库USART源代码解析及应用
STM32HAL库USART源代码解析 前言STM32CubeIDE配置串口USART和UART的选择使用模式参数设置GPIO配置DMA配置中断配置硬件流控制使能生成代码解析和使用方法串口初始化__UART_HandleTypeDef结构体浅析HAL库代码实际使用方法使用轮询方式发送使用轮询方式接收使用中断方式发送使用中…...
