【多线程进阶】CAS实现及应用

news2024/11/18 6:02:55

文章目录

  • 前言
  • 1. 什么是 CAS
  • 2. CAS 是如何实现的
  • 3. CAS 有哪些应用
    • 3.1 实现原子类
    • 3.2 实现自旋锁
  • 4. CAS 中 ABA 问题
    • 4.1 ABA 问题是什么
    • 4.2 ABA 引发的问题
    • 4.3 解决方案
  • 总结


前言

上文讲解 synchronized 当提到自旋锁时, 讲到当其他线程进入竞争, 偏向锁状态被消除, 就会进入轻量级锁状态(自适应的自旋锁) , 而 Java 中自旋锁其实背后原理就是 CAS 来实现的, 本文我们就来重点讲解一下 CAS 背后的机制.

关注收藏, 开始学习吧🧐


1. 什么是 CAS

CAS, 全称Compare and swap,字面意思: “比较并交换”, 能够比较和交换某个寄存器中的值, 和内存中的值是否相等. 如果相等, 则把另一个寄存器中的值和内存进行交换.

CAS 是单条 CPU 指令, 是不可拆分的.

我们假设内存中的原数据是V, 旧的预期值为A, 需要修改的新值为B.

一个 CAS 会涉及到以下操作:

  1. 比较 A 与 V 是否相等。(compare)
  2. 如果比较相等,将 B 写入 V。(swap)
  3. 返回操作是否成功。

一个 CAS 伪代码

// 下面写的代码并不是原子的, 真实的 CAS 是一个原子级的硬件指令完成的. 
// 这个伪代码只是为了辅助理解 CAS 的工作流程.
boolean CAS(address, expectValue, swapValue) {
 if (&address == expectedValue) {
   &address = swapValue;
        return true;
   }
    return false;
}

当多个线程同时对某个资源进行CAS操作, 只能有一个线程操作成功, 但是并不会阻塞其他线程, 其他线程只会收到操作失败的信号. 这对于我们编写线程安全代码, 又提供了一个全新的思路, 在之前我们通过上锁来实现线程安全, 而基于 CAS 又能实现一套 “无锁编程”, 不过 CAS 也有着下面的一些问题.

  1. CPU开销过大. 在并发量比较高的情况下, 如果许多线程反复尝试更新某一个变量, 却又一直更新不成功, 循环往复, 会给CPU带来很到的压力.
  2. 不能保证代码块的原子性. CAS机制所保证的知识一个变量的原子性操作, 而不能保证整个代码块的原子性. 当需要保证3个变量共同进行原子性的更新, 就不得不使用 synchronized 了.
  3. ABA问题. 这也是CAS机制最大的问题所在, 下文会重点讲解.

由于上述问题, 导致其使用范围还是有一定的局限性.

2. CAS 是如何实现的

针对不同的操作系统, JVM 用到了不同的 CAS 实现原理, 简单来讲:

  • Java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作.
  • unsafe 的 CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg.
  • Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子性.

简而言之, 是因为硬件予以了支持, 软件层面才能做到.

3. CAS 有哪些应用

3.1 实现原子类

标准库中提供了 java.util.concurrent.atomic 包, 里面的类都是基于这种方式来实现的. 典型的就是 AtomicInteger 类. 其中的 getAndIncrement 相当于 i++ 操作.
在这里插入图片描述
原子类中提供了自增, 自减, 自增任意值, 自减任意值这些操作, 都是基于 CAS 按照无锁编程来实现的.

伪代码实现:

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

现在假设有两个线程同时调用 getAndIncrement().

  1. 两个线程都读取 value 的值到 oldValue 中. (oldValue 是一个局部变量, 在栈上. 每个线程有自己的栈)
    在这里插入图片描述

  2. 线程1 先执行 CAS 操作. 由于 oldValue 和 value 的值相同, 直接进行对 value 赋值.
    在这里插入图片描述

  3. 线程2 再执行 CAS 操作, 第一次 CAS 的时候发现 oldValue 和 value 不相等, 不能进行赋值. 因此需要进入循环. 在循环里重新读取 value 的值赋给 oldValue.
    在这里插入图片描述

  4. 线程2 接下来第二次执行 CAS, 此时 oldValue 和 value 相同, 于是直接执行赋值操作.
    在这里插入图片描述

  5. 最后 线程1 和 线程2 返回各自的 oldValue 的值即可.

通过形如上述代码就可以实现一个原子类. 利用原子类, 我们不需要使用重量级锁, 就可以高效的完成多线程的自增操作, 实现无锁编程.

CAS 是通过识别当前是否会出现 “插队” 情况, 如果没有操作插队, 此时是安全的, 可以直接修改. 如果有操作进行插队, 就重新读取内存中最新的值, 再次尝试修改即可. 这与采取加锁保证线程安全的方法是不同的.

3.2 实现自旋锁

在之前的锁策略文章中, 在讲自旋锁时, 我们其实就提到了 CAS. 自旋锁其实就是基于 CAS 实现更灵活的锁, 能获取到更多的控制权.

自旋锁伪代码

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;
   }
}

当该锁已经处于加锁状态, while 循环这里就会返回 false, 循环条件成立, CAS 不会进行实际的交换操作, 进入下一轮循环, 一直到该锁被持有线程释放.

4. CAS 中 ABA 问题

CAS 的关键要点, 是比较 内存 和 寄存器 中的值, 通过比较是否相等来判定内存中的值是否发生了改变.

  • 如果内存中的值变了, 存在其他线程对内存中值进行了修改.
  • 如果内存中的值没变, 则认为没有其他线程进行修改.

但是, 如果内存中的值没有变化, 就一定没有别的线程修改吗?

这就要说一下 CSA 中的 ABA 问题.

4.1 ABA 问题是什么

假设存在两个线程 t1 和 t2. 有一个共享变量 num, 初始值为 A. 接下来, 线程 t1 想使用 CAS 把 num 值改成 Z, 那么就需要:

  • 先读取 num 的值, 记录到 oldNum 变量中.
  • 使用 CAS 判定当前 num 的值是否为 A, 如果为 A, 就修改成 Z.

但是, 在 t1 执行这两个操作之间, t2 线程可能把 num 的值从 A 改成了 B, 又从 B 改成了 A.

到这一步, t1 线程已经无法区分当前这个变量还是之前那个 A, 还是经历了一个变化过程之后的 A.
在这里插入图片描述
在当前情况下, 即使出现了 ABA 问题, 也没啥太大的影响. 但是如果遇到一些极端的场景, 就不一定了.

4.2 ABA 引发的问题

假设 A 有 100 存款. 他想从 ATM 取 50 块钱. 取款机创建了两个线程, 并发的来执行 -50 操作.

  • 我们期望一个线程执行 -50 成功, 另一个线程 -50 失败.
  • 如果使用 CAS 的方式来完成这个扣款过程就可能出现问题.

正常的过程

  1. 一开始存款有 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期望更新为 50.
  2. 线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中.
  3. 轮到线程2 执行了, 发现当前存款为 50, 和之前读到的 100 不相同, 执行失败.

异常的过程

  1. 存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期望更新为 50.
  2. 线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中.
  3. 在线程2 执行之前, A 的朋友正好给 A 转账 50, 账户余额变成了100.
  4. 轮到线程2 执行了, 发现当前存款为 100, 和之前读到的 100 相同, 再次执行扣款操作.

这个时候, 扣款操作会被执行了两次, 这就是 ABA 问题搞的鬼.

4.3 解决方案

由于 CAS 只是简单的判定 “值是否相同”, 但实际上想判定的是 “这个值有没有变化过”. 所以我们可以约定一个版本号来衡量内存中的值是否发生改变.

给要修改的值, 引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期.

CAS 操作在读取旧值的同时, 也要读取版本号. 真正修改的时候:

  • 如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1.
  • 如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了).

对比理解上面的转账例子
假设 A 有 100 存款. 滑稽想从 ATM 取 50 块钱. 取款机创建了两个线程, 并发的来执行 -50 操作. 我们期望一个线程执行 -50 成功, 另一个线程 -50 失败.

为了解决 ABA 问题, 我们给余额搭配一个版本号, 初始设为 1.

  1. 存款 100. 线程1 获取到 存款值为 100, 版本号为 1, 期望更新为 50; 线程2 获取到存款值为 100, 版本号为 1, 期望更新为 50.
  2. 线程1 执行扣款成功, 存款被改成 50, 版本号改为2. 线程2 阻塞等待中.
  3. 在线程2 执行之前, A 的朋友正好给 A 转账 50, 账户余额变成 100, 版本号变成3.
  4. 轮到线程2 执行了, 发现当前存款为 100, 和之前读到的 100 相同, 但是当前版本号为 3, 之前读到的版本号为 1, 版本小于当前版本, 认为操作失败.

在 Java 标准库中提供了 AtomicStampedReference<E> 类. 这个类可以对某个类进行包装, 在内部就提供了上面描述的版本管理功能.


总结

✨ 本文重点讲述了多线程中 CAS 知识, CAS 也是多线程开发中的一种典型思路, 但是本文中没有花时间介绍 Java 中提供的 CAS 的 api 怎么使用, 实际开发中, 一般是不会直接使用 CAS 的, 都是使用库里已经基于 CAS 封装好的组件, 比如原子类去实现.
✨ 想了解更多的多线程知识, 可以收藏一下本人的多线程学习专栏, 里面会持续更新本人的学习记录, 跟随我一起不断学习.
✨ 感谢你们的耐心阅读, 博主本人也是一名学生, 也还有需要很多学习的东西. 写这篇文章是以本人所学内容为基础, 日后也会不断更新自己的学习记录, 我们一起努力进步, 变得优秀, 小小菜鸟, 也能有大大梦想, 关注我, 一起学习.

再次感谢你们的阅读, 你们的鼓励是我创作的最大动力!!!!!

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

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

相关文章

简单聊一聊公平锁和非公平锁,parallel并行流

目录 一、降低锁的粒度&#xff0c;将synchronized关键字不放在方法上了&#xff0c;改为synchronized代码块。二、先区分一下公平锁和非公平锁1、公平锁2、非公平锁3、公平锁的优缺点&#xff1a;4、非公平锁的优缺点&#xff1a; 三、是否对症下药四、IntStream.rangeClosed是…

C++ - C++11历史 - 统一列表初始化 - aotu - decltype - nullptr - C++11 之后 STL 的改变

C的发展史了解 在2003年C标准委员会曾经提交了一份技术勘误表(简称TC1)&#xff0c;使得C03这个名字已经取代了C98称为C11之前的最新C标准名称。 不过由于C03(TC1)主要是对C98标准中的漏洞进行修复&#xff0c;语言的核心部分则没有改动&#xff0c;因此人们习惯性的把两个标…

15046-2011 脂肪酰二乙醇胺 学习笔记

声明 本文是学习GB-T 15046-2011 脂肪酰二乙醇胺.pdf而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 1 范围 本标准规定了脂肪酰二乙醇胺的产品分类、要求、试验方法、检验规则和标志、包装、运输、贮存和保 质期。 本标准适用于由脂肪酸甲酯或脂肪…

C++(反向迭代器)

前言&#xff1a; 上一章我们介绍了适配器&#xff0c;也提了一下迭代器适配器&#xff0c;今天我们就从反向迭代器把迭代器适配器给解释一下。 既然 都叫迭代器容器了 就说名只要接口合适他可以封装实现各种容器需求包括vector list 。 目录 1.反向迭代器设计 1.1反向迭代…

浅谈电气防火限流式保护器在小型人员密集场所中的应用

摘要&#xff1a;本文通过结合城市中小型人员密集场所的特点和电气防火限流式保护器的功能&#xff0c;阐述了该类筑物预防电气火灾事故的方法。 关键词&#xff1a;小型人员密集场所&#xff1b;电气防火限流式保护器 0&#xff1a;概述 近年来&#xff0c;随着社会经济的不…

C++标准模板(STL)- 类型支持 (定宽整数类型)(INT8_C,INTMAX_C,UINT8_C,UINTMAX_C,格式化宏常量)

最小宽度整数常量的函数宏 INT8_CINT16_CINT32_CINT64_C 展开成拥有其实参所指定的值且类型分别为 int_least8_t、int_least16_t、int_least32_t、int_least64_t 的整数常量表达式 (宏函数) INTMAX_C 展开成拥有其实参所指定的值且类型为 intmax_t 的整数常量表达式 (宏函数) U…

pytorch算力与有效性分析

pytorch Windows中安装深度学习环境参考文档机器环境说明3080机器 Windows11qt_env 满足遥感CS软件分割、目标检测、变化检测的需要gtrs 主要是为了满足遥感监测管理平台&#xff08;BS&#xff09;系统使用的&#xff0c;无深度学习环境内容swin_env 与 qt_env 基本一致od 用于…

力扣第100题 相同的数 c++ 二叉 简单易懂+注释

题目 100. 相同的树 简单 给你两棵二叉树的根节点 p 和 q &#xff0c;编写一个函数来检验这两棵树是否相同。 如果两个树在结构上相同&#xff0c;并且节点具有相同的值&#xff0c;则认为它们是相同的。 示例 1&#xff1a; 输入&#xff1a;p [1,2,3], q [1,2,3] 输出…

代码随想录算法训练营第五十九天 | 动态规划 part 17 | 647. 回文子串、516.最长回文子序列

目录 647. 回文子串思路思路2 双指针代码 516.最长回文子序列思路代码 647. 回文子串 Leetcode 思路 dp[i][j]&#xff1a;表示区间范围[i,j] &#xff08;注意是左闭右闭&#xff09;的子串是否是回文子串&#xff0c;如果是dp[i][j]为true&#xff0c;否则为false。递推公式…

python和go相互调用的两种方法

前言 Python 和 Go 语言是两种不同的编程语言&#xff0c;它们分别有自己的优势和适用场景。在一些项目中&#xff0c;由于团队内已有的技术栈或者某一部分业务的需求&#xff0c;可能需要 Python 和 Go 相互调用,以此来提升效率和性能。 性能优势 Go 通常比 Python 更高效&…

什么是DOM(Document Object Model)?如何使用JavaScript操作DOM元素?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ 什么是DOM&#xff1f;⭐ 如何使用JavaScript操作DOM元素&#xff1f;1. 获取DOM元素2. 修改元素内容3. 修改元素属性4. 添加和移除元素5. 添加和移除事件监听器 ⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界…

【开发篇】十七、消息:模拟订单短信通知

文章目录 1、消息2、JMS3、AMQP4、案例&#xff1a;模拟订单短信通知 相关文章&#xff1a; 【同步通讯与异步通讯】 1、消息 消息的发送方&#xff0c;即生产者。消息的接收方&#xff0c;即消费者。同步通信就行打视频&#xff0c;等着对方接电话才能继续往下&#xff0c;而…

JDBC 【SQL注入】

一、SQL注入&#x1f353; (一)、SQL注入问题&#x1f95d; 1.向jdbc_user表中 插入两条数据 # 插入2条数据 INSERT INTO jdbc_user VALUES(NULL,jack,123456,2020/2/24); INSERT INTO jdbc_user VALUES(NULL,tom,123456,2020/2/24);2.SQL注入演示 # SQL注入演示 -- 填写…

泊车功能专题介绍 ———— AVP系统基础数据交互内容

文章目录 系统架构系统功能描述云端子系统车辆子系统场端子系统用户APP 工作流程基础数据交互内容AVP 系统基础数据交互服务车/用户 - 云基础数据交互内容车位查询工作流程技术要求数据交互要求 车位预约工作流程技术要求数据交互要求 取消预约工作流程技术要求数据交互要求 泊…

利用C++开发一个迷你的英文单词录入和测试小程序-升级版本

我们现在有了一个本地sqlite3的迷你英文单词小测试工具&#xff0c;需求就跟工作当中一样是不断变更的。这里虚构两个场景&#xff0c;并且一步一步的完成最终升级后的小demo。 场景&#xff1a;数据不依赖本地sqlite3&#xff0c;需要支持远程访问&#xff0c;用目前的restfu…

深入探究C++编程中的资源泄漏问题

目录 1、GDI对象泄漏 1.1、何为GDI资源泄漏&#xff1f; 1.2、使用GDIView工具排查GDI对象泄漏 1.3、有时可能需要结合其他方法去排查 1.4、如何保证没有GDI对象泄漏&#xff1f; 2、进程句柄泄漏 2.1、何为进程句柄泄漏&#xff1f; 2.2、创建线程时的线程句柄泄漏 …

Dijkstra 邻接表表示算法 | 贪心算法实现--附C++/JAVA实现源码

以下是详细步骤。 创建大小为 V 的最小堆,其中 V 是给定图中的顶点数。最小堆的每个节点包含顶点编号和顶点的距离值。 以源顶点为根初始化最小堆(分配给源顶点的距离值为0)。分配给所有其他顶点的距离值为 INF(无限)。 当最小堆不为空时,执行以下操作: 从最小堆中提取…

JVM技术文档--JVM诊断调优工具Arthas--阿里巴巴开源工具--一文搞懂Arthas--快速上手--国庆开卷!!

​ Arthas首页 简介 | arthas Arthas官网文档 Arthas首页、文档和下载 - 开源 Java 诊断工具 - OSCHINA - 中文开源技术交流社区 阿丹&#xff1a; 之前聊过了一些关于JMV中的分区等等&#xff0c;但是有同学还是在后台问我&#xff0c;还有私信问我&#xff0c;学了这些…

人工智能驱动的古彝文识别:保护和传承古彝文文化

&#x1f935;‍♂️ 个人主页&#xff1a;艾派森的个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持&#xff0c;我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f4…

从零开始 Spring Cloud 13:分布式事务

从零开始 Spring Cloud 13&#xff1a;分布式事务 1.分布式事务问题 用一个示例项目演示在分布式系统中使用事务会产生的问题。 示例项目的 SQL&#xff1a;seata_demo.sql 示例项目代码&#xff1a;seata-demo.zip 这个示例项目中的微服务的互相调用依赖于 Nacos&#xf…