【多线程】详解 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.xdnf.cn/news/1556896.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

Rspamd:开源垃圾邮件过滤系统

Rspamd 是一个开源垃圾邮件过滤和电子邮件处理框架&#xff0c;旨在根据各种规则评估消息&#xff0c;包括正则表达式、统计分析以及与 URL 黑名单等自定义服务的集成。 系统会分析每封邮件并做出判定&#xff0c;MTA可据此采取进一步行动&#xff0c;例如拒绝邮件或添加垃圾邮…

低照度图像增强网络——EnlightenGAN

系列文章目录 GAN生成对抗网络介绍https://blog.csdn.net/m0_58941767/article/details/142704354?spm1001.2014.3001.5501 循环生成对抗网络——CycleGANhttps://blog.csdn.net/m0_58941767/article/details/142704671?spm1001.2014.3001.5501 目录 系列文章目录 前言 …

SSM社区慢性病管理系统—计算机毕业设计源码37572

摘 要 社区慢性病管理是社区卫生服务的主要内容&#xff0c;发展社区卫生服务是提供基本卫生服务、满足人民群众日益增长的卫生服务需求&#xff0c;也是提高人民健康水平的重要保障。为迎接慢性病防治的挑战我国进行了社区卫生服务改革&#xff0c;但由于社区卫生存在的诸多问…

OJ在线评测系统 微服务 OpenFeign调整后端下 nacos注册中心配置 不给前端调用的代码 全局引入负载均衡器

OpenFeign内部调用二 4.修改各业务服务的调用代码为feignClient 开启nacos注册 把Client变成bean 该服务仅内部调用&#xff0c;不是给前端的 将某个服务标记为“内部调用”的目的主要有以下几个方面&#xff1a; 安全性: 内部API通常不对外部用户公开&#xff0c;这样可以防止…

【CF2021E】Digital Village(All Version)

题目 给你一张 n n n 个点 m m m 条边的无向图&#xff0c;有 p p p 个关键点。你需要选择 k k k 个点染黑&#xff0c;使得这 p p p 个关键点到这 k k k 个黑点的代价和最小。定义代价为两点之间边权最大的边的最小值。 你需要求出 k 1,2,…,n 的所有答案 E1 n,m,p&l…

fiddler抓包20_弱网模拟

课程大纲 ① 打开CustomRules.js文件&#xff1a;Fiddler快捷键“CtrlR”(或鼠标点击&#xff0c;菜单栏 - Rules - Customize Rules)。 ② 设置速率&#xff1a;“CtrlF”&#xff0c;搜索 “m_SimulateModem”&#xff0c;定位至函数。在函数里设置上传、下载速率&#xff0c…

乔斯编程——P3283 通信救援

说明 众所周知&#xff0c;在同一平面内到定点的距离等于定长的点的集合叫做圆。这个定点叫做圆的圆心&#xff0c;定长即圆的半径。 同时用圆心的坐标和圆的半径&#xff0c;就可以确定圆在平面内的位置&#xff0c; 在本题当中&#xff0c;我们根据圆在平面覆盖的区域来描述…

全面解析大型模型Agent智能体原理及实践案例

1 什么是大模型 Agent &#xff1f; 大模型 Agent&#xff0c;作为一种人工智能体&#xff0c;是具备环境感知能力、自主理解、决策制定及执行行动能力的智能实体。简而言之&#xff0c;它是构建于大模型之上的计算机程序&#xff0c;能够模拟独立思考过程&#xff0c;灵活调…

动态规划10:174. 地下城游戏

动态规划解题步骤&#xff1a; 1.确定状态表示&#xff1a;dp[i]是什么 2.确定状态转移方程&#xff1a;dp[i]等于什么 3.初始化&#xff1a;确保状态转移方程不越界 4.确定填表顺序&#xff1a;根据状态转移方程即可确定填表顺序 5.确定返回值 题目链接&#xff1a;174.…

【FPGA】面试八股

1.FPGA的底层资源有哪些 &#xff08;1&#xff09;可编程的逻辑资源 可编程的逻辑单元由查找表&#xff08;LUT&#xff09;,数据选择器&#xff08;MUX&#xff09;,进位链&#xff08;Carry Chain&#xff09;和触发器&#xff08;Flip-Flop&#xff09; &#xff08;2&…

毕业设计——物联网设备管理系统后台原型设计

作品详情 主要功能&#xff1a; 通过构建数字化综合体&#xff0c;利用物联网技术、设备监控技术采集生产线设备等物对象的实时数据&#xff0c;加强信息汇聚管理和服务&#xff0c;多系统维度、多层次的清楚地掌握设施各系统的状态&#xff0c;提高厂房服务的可控性、安全性&…

算法剖析:双指针

文章目录 双指针算法一、 移动零1. 题目2. 算法思想3. 代码实现 二、 复写零1. 题目2. 算法思想3. 代码实现 三、 快乐数1. 题目2. 算法思想3. 代码实现 四、 盛水最多的容器1. 题目2. 算法思想3. 代码实现 五、有效三角形的个数1. 题目2. 算法思想3. 代码实现 六、 和为 s 的两…

出国必备神器!这5款中英翻译工具让你秒变外语达人

在这个全球化的时代&#xff0c;中英互译已然成为我们日常生活和工作中不可或缺的一环。面对众多的翻译工具&#xff0c;如何选择一款既高效又人性化的翻译助手呢&#xff1f;今天&#xff0c;就让我为大家揭秘几款热门的中英互译工具&#xff0c;并分享我的使用感受。 一、福昕…

中广核CGN25届校招网申SHL测评题库、面试流程、招聘对象,内附人才测评认知能力真题

​中国广核集团校园招聘在线测评攻略&#x1f680; &#x1f393; 校园招聘对象 2024届、2025届海内外全日制应届毕业生&#xff0c;大专、本科、硕士、博士&#xff0c;广核集团等你来&#xff01; &#x1f4c8; 招聘流程 投递简历 简历筛选 在线测评&#xff08;重点来啦…

用java编写飞机大战

游戏界面使用JFrame和JPanel构建。背景图通过BG类绘制。英雄机和敌机在界面上显示并移动。子弹从英雄机发射并在屏幕上移动。游戏有四种状态&#xff1a;READY、RUNNING、PAUSE、GAMEOVER。状态通过鼠标点击进行切换&#xff1a;点击开始游戏&#xff08;从READY变为RUNNING&am…

详解Redis分布式锁在SpringBoot的@Async方法中没锁住的坑

背景 Redis分布式锁很有用处&#xff0c;在秒杀、抢购、订单、限流特别是一些用到异步分布式并行处理任务时频繁的用到&#xff0c;可以说它是一个BS架构的应用中最高频使用的技术之一。 但是我们经常会碰到这样的一个问题&#xff0c;那就是我们都按照标准做了但有时运行着、…

JavaEE之多线程进阶-面试问题

一.常见的锁策略 锁策略不是指某一个具体的锁&#xff0c;所有的锁都可以往这些锁策略中套 1.悲观锁与乐观锁 预测所冲突的概率是否高&#xff0c;悲观锁为预测锁冲突的概率较高&#xff0c;乐观锁为预测锁冲突的概率更低。 2.重量级锁和轻量级锁 从加锁的开销角度判断&am…

【Docker】03-自制镜像

1. 自制镜像 2. Dockerfile # 基础镜像 FROM openjdk:11.0-jre-buster # 设定时区 ENV TZAsia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone # 拷贝jar包 COPY docker-demo.jar /app.jar # 入口 ENTRYPOINT ["ja…

【强训笔记】day26

NO.1 思路&#xff1a;只需判断长度为2和3的回文子串。 代码实现&#xff1a; #include<iostream> #include<string>using namespace std;string s;int main() {cin>>s;int ns.size(),ret-1;for(int i0;i<n;i){if(i1<n&&s[i]s[i1]){ret2;}i…

笔试题总结

1.对于线性表的描述&#xff1a;存储空间不一定是连续&#xff0c;且各元素的存储顺序是任意的 2.虚函数的定义&#xff1a;函数的返回值参数不定&#xff0c; 声明&#xff1a; 类型&#xff0c;返回这类型 名字&#xff08;&#xff09;&#xff1b; 例如声明一个虚函数&a…