【多线程】详解 CAS 机制

🥰🥰🥰来都来了,不妨点个关注叭!
👉博客主页:欢迎各位大佬!👈

在这里插入图片描述

文章目录

  • 1. CAS 是什么
    • 1.1 CAS 具体步骤
    • 1.2 CAS 伪代码
  • 2. CAS 的应用
    • 2.1 实现原子类
      • 2.1.1 AtomInteger 类
      • 2.1.2 伪代码实现原子类
    • 2.2 实现自旋锁
      • 2.2.1 自旋锁是什么
      • 2.2.2 伪代码实现自旋锁
  • 3. CAS 的 ABA 问题
    • 3.1 ABA 问题
    • 3.2 ABA 问题引起的 BUG
    • 3.2 ABA 问题的解决方案 —— 使用版本号

1. CAS 是什么

CAS】全称为 Compare and swap,即"比较并交换",相当于通过一个原子操作,同时完成"读取内存,比较是否相等,修改内存"这三个步骤,本质上需要 CPU 指令支持~

CAS 是并发编程中一个重要的概念,相当于是打开了新世界的大门,可以在不加锁的情况下保证线程安全,从而减少线程之间的竞争和开销,通常用于无锁编程,本文将结合 Java 多线程操作讲解 CAS 机制,我们一起来看看吧!

1.1 CAS 具体步骤

CAS 机制的基本思想是,先比较内存 V 中的值与寄存器 A 中的值(旧的预期值),是否相等,如果相等,则将寄存器 B 中的值(需要修改的新值)写入内存中,如果不相等,则不作任何操作,这整个过程是原子的~

CAS 涉及到以下三个操作,假设内存中的原数据V,旧的预期值A,需要修改的新值B

  • 读取内存值:将需要修改的值从主内存中读入本地线程缓存或工作内存
  • 比较并尝试交换:比较 A 与 V 是否相等,如果比较相等,将 B 写入 V,如果不相等,不作任何操作
  • 返回操作结果:如果成功更新了值,则返回成功标志或新值,如果失败更新,则返回失败标志或当前内存中的值

1.2 CAS 伪代码

如果把 CAS 想象成一个函数,可以得到 CAS 的伪代码,但是下述的伪代码,并不是真正的 CAS 代码,事实上,CAS 操作是一条由 CPU 硬件支持、原子的硬件指令,而这一条指令就可以完成下述这一段代码的功能(CAS 本身就是对应一条 CPU 指令,不可拆分的最小单位,此时,CAS 中比较和交换动作是没办法再拆分的)

boolean CAS(address,expectValue,swapValue) {if(&address == expectedValue) {&address = swapValue;return true;}return false;
}

图解如下:
在这里插入图片描述
可以知道,上述这一段代码,非原子,运行过程中可能随着线程的调度有概率产生线程安全问题,而原子指令不会有线程安全问题~

同时,CAS也不会有内存可见性的问题,内存可见性是编译器把一系列指令进行调整,把读内存指令调整成直接读寄存器的指令,效率大大提升,可能会误判,从而产生 bug,但是 CAS 本身就是指令级别读取内存的操作,因此,不会有内存可见性带来的线程安全问题

CAS 可以不加锁也能一定程度保证线程安全!这样就可以基于 CAS 机制,实现一系列操作

2. CAS 的应用

2.1 实现原子类

2.1.1 AtomInteger 类

标准库在 java.util.concurrent.atomic 包里提供很多类使用高效的指令来保证操作的原子性,而不是使用加锁来保证,其中提供 AtomInteger 类,能够以原子方式保证一个整数自增或自减操作的线程安全~

AtomInteger 类提供如下 4 个方法:

 getAndIncrement(); //后置++incrementAndGet();	//前置++getAndDecrement();	//后置--decrementAndGet();	//前置--
public class ThreadDemo {public static void main(String[] args) {AtomicInteger num = new AtomicInteger(0);Thread t1 = new Thread(()->{//num++num.getAndIncrement();System.out.println(num.get());//++numnum.incrementAndGet();System.out.println(num.get());//num--num.getAndDecrement();System.out.println(num.get());//--numnum.decrementAndGet();System.out.println(num.get());});t1.start();}
}

打印结果如下:

在这里插入图片描述

public class ThreadDemo35 {public static void main(String[] args) throws InterruptedException {AtomicInteger num = new AtomicInteger(0);Thread t1 = new Thread(()->{for(int i = 0; i < 10000; i++) {//num++num.getAndIncrement();}});Thread t2 = new Thread(()->{for(int i = 0; i < 10000; i++) {//num++num.getAndIncrement();}});t1.start();t2.start();t1.join();t2.join();System.out.println(num.get());}
}

运行结果如下:最终 num 的值为 20000(固定的)

在这里插入图片描述
原因
getAndIncrement() 方法以原子的方式获得 num 的值,并将 num 进行 ++ 自增操作,即获得值、并将该值增加1,再生成新值,这一整个操作,是原子的,不会被中断,就可以保证在多线程编程环境下,并发地访问同一个实例,计算可以返回正确的值~

我们可以查看源码,发现 getAndIncrement() 方法并没有使用加锁(synchronized)的操作来保证原子性,如下:

1)先点进 getAndIncrement() 方法源码

在这里插入图片描述
2)再点进 getAndAddInt() 方法源码,可以看到,其中使用了 CAS 机制
在这里插入图片描述
3)再点进 compareAndSwapInt() 方法,可以发现,这是一个由 native 修饰的方法,CAS 机制的实现依赖于底层硬件和操作系统提高的原子操作支持,它是更偏向底层的操作~

在这里插入图片描述


上述是线程安全的案例,接着,线程不安全案例,与之形成对比,代码如下:
class Counter {private int count = 0;//count++操作public void add() {count++;}//得到count的值public int get() {return count;}
}public class ThreadDemo {public static void main(String[] args) throws InterruptedException {Counter counter = new Counter();Thread t1 = new Thread(() -> {for (int i = 0; i < 10000; i++) {counter.add();}});Thread t2 = new Thread(() -> {for (int i = 0; i < 10000; i++) {counter.add();}});t1.start();t2.start();t1.join();t2.join();System.out.println(counter.get());}
}

运行的结果,如下:
在这里插入图片描述
原因
在实际中,线程的调度顺序是无序的,不能确定 t1 和 t2 线程在自增过程中是如何执行的,因此,结果是不确定的~ 归根结底是线程无序调度的锅!(具体分析可以回顾往期内容:线程安全)

2.1.2 伪代码实现原子类

伪代码如下:

class AtomicInteger {private int value;public int getAndIncrement() {int oldValue = value;while (CAS(value, oldValue, oldValue+1) != true) {oldValue = value;}return oldValue;}
}

图解如下:
在这里插入图片描述
但是会不会出现这样一个情况:在上述代码中,上面刚刚把 value 赋值给 oldValue,紧接着这里的比较,value 和 oldValue 这两个值会不相等吗?

答案是:绝对会!因为在多线程下,线程是无序调度的,value 是成员变量,如果两个线程同时调用
getAndIncrement() 方法,就有可能出现不相等的情况!

其实,此处的 CAS 就是在确认,看当前 value 是不是变过!如果没变过,才能自增,如果变过了,则先更新,再自增~

之前线程不安全,很大的云因是一个线程不能及时感知到另一个线程对内存的修改,比如 t2 在自增的时候,先读后自增,此时在自增之前,t1 线程已经自增过了,t2 线程在 0 的基础上自增的,导致无效自增,就会出现问题,使用 CAS 后, t2 会在自增之前,先检查一下寄存器的值和内存的值是否一致!只有一致才会执行自增,否则重新将内存中的值更新,与寄存器同步~

这个操作不涉及阻塞等待,因此,会比加锁解决线程不安全问题的方案快很多

2.2 实现自旋锁

2.2.1 自旋锁是什么

自旋锁
自旋锁是一种忙等待的锁机制,如果获取锁失败,立即再尝试获取锁,无限循环,直到获取到锁为止,第一次获取锁失败,第二次的尝试会在极短的时间内到来,一旦锁被其他线程释放,就能第一时间获取到锁,一般乐观锁的情况下,锁冲突概率低,实现自旋锁比较合适~ 实现自旋,目的就是为了忙等,就是为了能够最快的速度拿到锁!
(可回顾这一期内容:常见的锁策略)

2.2.2 伪代码实现自旋锁

public class SpinLock {private Thread owner = null;public void lock(){// 通过CAS查看当前锁是否被某个线程持有// 如果这个锁已经被别的线程持有,那么就自旋等待// 如果这个锁没有被别的线程持有,那么就把owner为当前尝试加锁的线程while(!CAS(this.owner, null, Thread.currentThread())){}}public void unlock (){this.owner = null;}
}

图解如下:
在这里插入图片描述

  • owner 记录当前的锁被哪个线程持有,如果为null,则说明没有线程持有,锁处于空闲状态

  • 比较 owner 和 null 是否相等(即判断 owner 是否为 null,锁是否为空闲状态)

    如果是相等,owner 是为 null,则进行交换,将当前线程的引用交换到 owner 中,加锁完成,交换成功则返回 true,循环就结束

    如果是不相等,意味着 owner 不为 null,锁已经有线程持有了,此时 CAS 就什么都不用做,返回 fasle,循环继续

    此时这个循环就会转得飞快,不停地尝试询问这里的锁是不是被释放了,一旦释放,就能立即获取到锁(实现自旋锁),坏处就是造成了 CPU 忙等

  • unlock()方法,解锁操作,即直接把 owner 设置为 null

CAS 机制,解释了自旋锁的实现~

3. CAS 的 ABA 问题

3.1 ABA 问题

CAS 的 ABA 问题,是使用 CAS 时会遇到的一个经典问题

CAS 的关键是对比内存和寄存器的值,看是否相同,就是通过这个比较来检测内存中的值是否发生改变

ABA 问题】可能存在这样一个情况,对比的时候是相同的,但其值不是没有变过,而是从A值变成B值又变回A值(A->B->A),此时,有一定概率会出问题,CAS 只能对比值是否相同,但不能确定这个值是否中间发生过改变(这就类似于在某鱼上买个手机,结果是个"翻新机"是一样)

图解如下:

在这里插入图片描述

3.2 ABA 问题引起的 BUG

ABA 问题,大部分情况下,都没事,但是,小概率下会出现 BUG!!!

假设这样一场景:取钱(这可是不能出现 BUG 的噢,毕竟对钱都很敏感hh)

小万有 1000 元存款,她要从 ATM 机中取出 500 元钱,此时,取款机创建了两个线程 t1、t2,并发执行 账户 -500 的操作

我们期望的是 —— 这两个线程中,一个线程执行 -500 的操作,而另一个线程 -500 失败

如果是 CAS 的方式来完成这个扣款,就可能出现问题,引发 BUG

  • 正常情况
    1)存款1000,线程 t1 获取到当前存款 1000,期望值更新为 500;线程 t2 获取到当前存款 1000,期望值更新为 500
    2)线程 t1 执行扣款成功,账户减少 500,存款变为 500,线程 t2 阻塞等待
    3)线程 t2 执行,当前存款为 500,与之前读到的 1000,对比,不相同,执行失败
  • 异常情况
    1)存款1000,线程 t1 获取到当前存款 1000,期望值更新为 500;线程 t2 获取到当前存款 1000,期望值更新为 500
    2)线程 t1 执行扣款成功,账户减少 500,存款变为 500,线程 t2 阻塞等待
    3)线程 t2 执行前,小万的朋友小丁,转给小万 500,此时小万的账户余额还是 1000!
    4)线程 t2 执行,当前存款为 1000,与之前读到的 1000,对比,相同,再次执行扣款操作

此时,扣款 500 的操作被执行了两次!这是 ABA 问题引起的 BUG

如何解决 ABA 问题呢?

3.2 ABA 问题的解决方案 —— 使用版本号

ABA 关键是值会反复横跳,如果约定数据只能单方向变化,即数据只能增加,或者只能减少,问题就迎刃而解了

  • Q:如果需求要求该数值,既能增加也能减少,那怎么解决呢?
  • A:可以引入另外一个版本号变量,约定版本号只能增加,每次修改,都会增加一个版本号就能感知值是否发生变化

每次 CAS 对比,就不是对比数值本身,而是对比版本号

只要约定版本号,只能递增,就能保证此时不会出现 ABA 反复横跳问题,以版本号为基准,而不是以变量数值为基准了(即 CAS 在对比的时候,对比的不是数值本身,而是对比版本号,这样其它线程在进行 CAS 操作时,可以检查版本号是否发生变化,从而避免 ABA 问题)

图解如下:

在这里插入图片描述

💛💛💛本期内容回顾💛💛💛

在这里插入图片描述
✨✨✨本期内容到此结束啦~

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

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

相关文章

word无法复制粘贴

word无法复制粘贴 使用word时复制粘贴报错 如下&#xff1a; 报错&#xff1a;运行时错误‘53’&#xff0c;文件未找到&#xff1a;MathPage.WLL 这是mathtype导致的。 解决方法 1&#xff09;在mathtype下载目录下找到"\MathType\MathPage\64"下的"mathpa…

Qt开发第一讲

一、Qt项目里面有什么&#xff1f; 对各个文件的解释&#xff1a; Empty.pro文件 QT core gui # 要引入的Qt模块&#xff0c;后面学习到一些内容的时候可能会修改这里 #这个文件相当于Linux里面的makefile文件。makefile其实是一个非常古老的技术了。 #qmake搭配.pr…

C++之模版进阶篇

目录 前言 1.非类型模版参数 2.模版的特化 2.1概念 2.2函数模版特化 2.3 类模板特化 2.3.1 全特化和偏特化 2.3.2类模版特化应用实例 3.模版分离编译 3.1 什么是分离编译 3.2 模板的分离编译 3.3 解决方法 4. 模板总结 结束语 前言 在模版初阶我们学习了函数模版和类…

【MySQL】Ubuntu环境下MySQL的安装与卸载

目录 1.MYSQL的安装 2.MySQL的登录 3.MYSQL的卸载 4.设置配置文件 1.MYSQL的安装 首先我们要看看我们环境里面有没有已经安装好的MySQL 我们发现是默认是没有的。 我们还可以通过下面这个命令来确认有没有mysql的安装包 首先我们得知道我们当前的系统版本是什么 lsb_…

你还在为教学资料转换烦恼吗?4款神器安利给你,PDF转JPG一键搞定

工作或者学习的时候&#xff0c;我们经常得把PDF文件转换成JPG图片。可能是因为在手机上看起来方便&#xff0c;或者是想放到PPT里展示&#xff0c;反正把PDF转JPG的情况挺多的。那有什么好用的软件能做这个转换呢&#xff1f;今天我就给你们介绍几个好用的。 1. 福昕PDF高质量…

儿童需要学习C++多久才能参加信息学奥赛的CSP-J比赛?

信息学奥赛&#xff08;NOI&#xff09;是国内编程竞赛领域的顶尖赛事&#xff0c;而对于初学者来说&#xff0c;参加NOI的第一步通常是通过CSP-J&#xff08;全国青少年信息学奥林匹克联赛初赛&#xff09;&#xff0c;这也是面向青少年程序员的入门级竞赛。作为信息学奥赛的基…

【解决办法】git clone报错unable to access ‘xxx‘: SSL certificate problem:

使用git clone 时报错unable to access xxx: SSL certificate problem: 这个报错通常是由于SSL证书问题引起的。通常可以按照以下步骤进行排查&#xff1a; 检查网络连接&#xff1a;确保你的网络连接正常&#xff0c;可以访问互联网。尝试使用其他网站或工具测试网络连接是否正…

基于SpringBoot vue3 的山西文旅网java网页设计与实现

博主介绍&#xff1a;专注于Java&#xff08;springboot ssm springcloud等开发框架&#xff09; vue .net php phython node.js uniapp小程序 等诸多技术领域和毕业项目实战、企业信息化系统建设&#xff0c;从业十五余年开发设计教学工作☆☆☆ 精彩专栏推荐订阅☆☆☆☆…

LeetCode讲解篇之1043. 分隔数组以得到最大和

文章目录 题目描述题解思路题解代码题目链接 题目描述 题解思路 对于这题我们这么考虑&#xff0c;我们选择以数字的第i个元素做为分隔子数组的右边界&#xff0c;我们需要计算当前分隔子数组的长度为多少时能让数组[0, i]进行分隔数组的和最大 我们用数组f表示[0, i)区间内的…

docker 部署 Seatunnel 和 Seatunnel Web

docker 部署 Seatunnel 和 Seatunnel Web 说明&#xff1a; 部署方式前置条件&#xff0c;已经在宿主机上运行成功运行文件采用挂载宿主机目录的方式部署SeaTunnel Engine 采用的是混合模式集群 编写Dockerfile并打包镜像 Seatunnel FROM openjdk:8 WORKDIR /opt/seatunne…

自动驾驶-问题笔记-待解决

参考线的平滑方法 参考线平滑算法主要有三种&#xff1a; 离散点平滑&#xff1b;螺旋曲线平滑&#xff1b;多项式平滑&#xff1b; 参考链接&#xff1a;参考线平滑 对于平滑方法&#xff0c;一直不太理解平滑、拟合以及滤波三者的作用与区别&#xff1b; 规划的起点&#x…

leetcode第189题:轮转数组(C语言版)

思路1&#xff08;不推荐&#xff09; 保存数组最后一个元素&#xff0c;然后数组全体元素后移一位&#xff0c;把保存的最后一个元素存放到数组的第一个位置&#xff0c;重复这一操作&#xff0c;直到执行完了k次。 时间复杂度&#xff1a;需要用k次循环&#xff0c;里面套一层…

MySQL的驱动安装

1、下载并安装MySQL 下载地址&#xff1a; 建议在下列框中选择LTS长期支持版本&#xff0c;下载对应的MSI安装文件。 安装完成后&#xff0c;将MySQL的环境bin路径添加到环境变量中。 可以运行MySQL Configurator进行配置&#xff0c;主要设置密码&#xff0c;并初始化。其余…

【五分钟学会】YOLO11 自定义数据集从训练到部署

数据集地址 数据集包含 360 张红血细胞图像及其注释文件&#xff0c;分为训练集与验证集。训练文件夹包含 300 张带有注释的图像。测试和验证文件夹都包含 60 张带有注释的图像。我们对原始数据集进行了一些修改以准备此 CBC 数据集&#xff0c;并将数据集分成三部分。在360张…

<<迷雾>> 第8章 学生时代的走马灯(3)--走马灯 示例电路

几个首尾相连的触发器使用同一个控制端&#xff0c;能同时触发 info::操作说明 鼠标单击开关切换开合状态 注: 其中 CP 为按钮开关, 每点击一次, Q 的输出前进一级 注: 第一个触发器的输出端 Q 需要先置入高电平. 如果重置了电路, 可外接电源先使第一个 Q 置入高电平. 另: 因为…

【Windows】在任务管理器中隐藏进程

在此前的一篇&#xff0c;我们已经介绍过了注入Dll 阻止任务管理器结束进程 -- Win 10/11。本篇利用 hook NtQuerySystemInformation 并进行断链的方法实现进程隐身&#xff0c;实测支持 taskmgr.exe 的任意多进程隐身。 任务管理器 代码&#xff1a; // dllmain.cpp : 定义 …

【中间件学习】Git的命令和企业级开发

一、Git命令 1.1 创建Git本地仓库 仓库是进行版本控制的一个文件目录。我们要想对文件进行版本控制&#xff0c;就必须创建出一个仓库出来。创建一个Git本地仓库对应的命令是 git init &#xff0c;注意命令要在文件目录下执行。 hrxlavm-1lzqn7w2w6:~/gitcode$ pwd /home/hr…

No.10 笔记 | PHP学习指南:PHP数组掌握

本指南为PHP开发者提供了一个全面而简洁的数组学习路径。从数组的基本概念到高级操作技巧&#xff0c;我们深入浅出地解析了PHP数组的方方面面。无论您是初学者还是寻求提升的中级开发者&#xff0c;这份指南都能帮助您更好地理解和运用PHP数组&#xff0c;提高编码效率和代码质…

python之运算符

1、算术运算符 算术运算符常用的有&#xff1a;&#xff0c;-&#xff0c;*&#xff0c; &#xff0c;/&#xff0c;//&#xff0c;%&#xff0c;>>,<< 1.1、加 常见的是算术相加&#xff0c;还有一种是字符串拼接。 a 10 b 20 print(a b) c "My &quo…

基于facefusion的换脸

FaceFusion是一个引人注目的开源项目&#xff0c;它专注于利用深度学习技术实现视频或图片中的面部替换。作为下一代换脸器和增强器&#xff0c;FaceFusion在人脸识别和合成技术方面取得了革命性的突破&#xff0c;为用户提供了前所未有的视觉体验。 安装 安装基础软件 安装…