【多线程】CAS详解

目录

  • 🌴什么是 CAS
    • 🌸CAS 伪代码
  • 🎍CAS 是怎么实现的
  • 🍀CAS 有哪些应⽤
    • 🌸实现原子类
    • 🌸实现自旋锁
  • 🌳CAS 的 ABA 问题
    • 🌸**什么是 ABA 问题**?
    • 🌸ABA 问题引来的 BUG
    • 🌸解决方案
  • ⭕相关面试题
  • 🚩总结

🌴什么是 CAS

CAS: 全称Compare and swap字⾯意思:”⽐较并交换“,⼀个 CAS 涉及到以下操作:

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

  1. ⽐较 A 与 V 是否相等。(⽐较)
  2. 如果⽐较相等,将 B 写⼊ V。(交换)
  3. 返回操作是否成功。

🌸CAS 伪代码

下⾯写的代码不是原⼦的, 真实的 CAS 是⼀个原⼦的硬件指令完成的. 这个伪代码只是辅助理解 CAS的⼯作流程.

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

下面看两种典型的不是原子性的代码:

  1. check and set (if 判定然后设定值) [上⾯的 CAS 伪代码就是这种形式]
  2. read and update (i++) [之前我们讲线程安全的代码例⼦是这种形式]

CAS原子性的作用

当多个线程同时对某个资源进⾏CAS操作,只能有⼀个线程操作成功,但是并不会阻塞其他线程,其他
线程只会收到操作失败的信号

所以

CAS 可以视为是⼀种乐观锁. (或者可以理解成 CAS 是乐观锁的⼀种实现⽅式)

🎍CAS 是怎么实现的

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

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

简⽽⾔之,是因为硬件予以了⽀持,软件层⾯才能做到

🍀CAS 有哪些应⽤

🌸实现原子类

标准库中提供了 java.util.concurrent.atomic 包, ⾥⾯的类都是基于这种⽅式来实现的.
典型的就是 AtomicInteger 类. 其中的 getAndIncrement 相当于 i++ 操作.

AtomicInteger atomicInteger = new AtomicInteger(0);
// 相当于 i++
atomicInteger.getAndIncrement();

伪代码实现:

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 赋值.

注意:
• CAS 是直接读写内存的, ⽽不是操作寄存器.
• CAS 的读内存, ⽐较, 写内存操作是⼀条硬件指令, 是原⼦的.

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

在循环⾥重新读取 value 的值赋给 oldValue
在这里插入图片描述
4. 线程2 接下来第⼆次执⾏ CAS, 此时 oldValue 和 value 相同, 于是直接执⾏赋值操作.

在这里插入图片描述
5. 线程1 和 线程2 返回各⾃的 oldValue 的值即可.

通过形如上述代码就可以实现⼀个原⼦类. 不需要使⽤重量级锁, 就可以⾼效的完成多线程的⾃增操作.

本来 check and set 这样的操作在代码⻆度不是原⼦的. 但是在硬件层⾯上可以让⼀条指令完成这个
操作, 也就变成原⼦的了.

🌸实现自旋锁

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

🌳CAS 的 ABA 问题

🌸什么是 ABA 问题

通过下面的例子理解ABA问题:

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

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

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

线程 t1 的 CAS 是期望 num 不变就修改. 但是 num 的值已经被 t2 给改了. 只不过⼜改成 A 了. 这个时
候 t1 究竟是否要更新 num 的值为 Z 呢?

到这⼀步, t1 线程⽆法区分当前这个变量始终是 A, 还是经历了⼀个变化过程.

在这里插入图片描述
这就好⽐, 我们买⼀个⼿机, ⽆法判定这个⼿机是刚出⼚的新⼿机, 还是别⼈⽤旧了, ⼜翻新过的⼿机.

🌸ABA 问题引来的 BUG

⼤部分的情况下, t2 线程这样的⼀个反复横跳改动, 对于 t1 是否修改 num 是没有影响的. 但是不排除⼀些特殊情况.

假设 滑稽⽼哥 有 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 执⾏之前, 滑稽的朋友正好给滑稽转账 50, 账⼾余额变成 100 !!
  4. 轮到线程2 执⾏了, 发现当前存款为 100, 和之前读到的 100 相同, 再次执⾏扣款操作

这个时候, 扣款操作被执⾏了两次!!! 都是 ABA 问题搞的⻤!!

🌸解决方案

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

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

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

这就好⽐, 判定这个⼿机是否是翻新机, 那么就需要收集每个⼿机的数据, 第⼀次挂在电商⽹站上的⼿
机记为版本1, 以后每次这个⼿机出现在电商⽹站上, 就把版本号进⾏递增. 这样如果买家不在意这是翻
新机, 就买. 如果买家在意, 就可以直接略过.

对⽐理解上⾯的转账例子

假设 滑稽⽼哥 有 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 执⾏之前, 滑稽的朋友正好给滑稽转账 50, 账⼾余额变成 100, 版本号变成3.
  4. 轮到线程2 执⾏了, 发现当前存款为 100, 和之前读到的 100 相同, 但是当前版本号为 3, 之前读到的版本号为 1, 版本⼩于当前版本, 认为操作失败.

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

关于 AtomicStampedReference 的具体⽤法此处不再展开. 有需要的同学⾃⾏查找⽂档了
解使⽤⽅法即可.

⭕相关面试题

  1. 讲解下你⾃⼰理解的 CAS 机制

全称 Compare and swap, 即 “⽐较并交换”. 相当于通过⼀个原⼦的操作, 同时完成 “读取内存, ⽐较是
否相等, 修改内存” 这三个步骤. 本质上需要 CPU 指令的⽀撑

2.ABA问题怎么解决?

给要修改的数据引⼊版本号. 在 CAS ⽐较数据当前值和旧值的同时, 也要⽐较版本号是否符合预期. 如
果发现当前版本号和之前读到的版本号⼀致, 就真正执⾏修改操作, 并让版本号⾃增; 如果发现当前版
本号⽐之前读到的版本号⼤, 就认为操作失败

🚩总结

关于《【多线程】CAS详解》就讲解到这儿,感谢大家的支持,欢迎各位留言交流以及批评指正,如果文章对您有帮助或者觉得作者写的还不错可以点一下关注,点赞,收藏支持一下!

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

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

相关文章

智慧城市建设的新里程碑:公共服务电子支付大屏

随着科技的飞速发展,我们的生活正在经历前所未有的变革。电子支付的出现,无疑是这场变革中的一大亮点,它不仅改变了我们日常的支付方式,更成为智慧城市建设的重要一环,为公众提供了更加便捷、高效的服务体验。 在以前&…

政府采购标书制作的要点解析

导语:政府采购是政府为满足公共利益,按照法定程序和标准,通过招标、竞争性谈判等方式,购买商品、工程和服务的行为。标书作为政府采购活动中的重要文件,其制作质量直接影响到项目的顺利进行。本文将围绕政府采购标书制…

二路归并排序的算法设计和复杂度分析and周记

数据结构实验报告 实验目的: 通过本次实验,了解算法复杂度的分析方法,掌握递归算法时间复杂度的递推计算过程。 实验内容: 二路归并排序的算法设计和复杂度分析 实验过程: 1.算法设计 第一步,首先要将数组进行…

【网站项目】314学生二手书籍交易平台

🙊作者简介:拥有多年开发工作经验,分享技术代码帮助学生学习,独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。🌹赠送计算机毕业设计600个选题excel文件,帮助大学选题。赠送开题报告模板&#xff…

JVM相关面试题(2024大厂高频面试题系列)

一、JVM的组成 1、JVM由哪些部分组成,运行流程是什么? 回答:在JVM中共有四大部分,分别是Class Loader(类加载器)、Runtime Data Area(运行时数据区,内存分区)、Execut…

MyBatis的补充用法

说明:之前介绍过MyBatis的用法,像 用注解和Mapper.xml操作数据库、在Mapper.xml里写动态SQL。最近在一次用MyBatis批量更新数据库对象的场景中,意识到对MyBatis的一些标签用法不太熟悉,所以去 MyBatis官网 看了一些文档&#xff0…

php httpfs链接hdfs

一.代码(有bug) GitHub - michaelbutler/php-WebHDFS: A PHP client for WebHDFS 二.调用代码 1.代码1.代码 require_once(../webhdfs/src/org/apache/hadoop/WebHDFS.php);require_once(../webhdfs/src/org/apache/hadoop/tools/Curl.php); require_o…

什么是人才储备?如何做人才储备?

很多小伙伴都会有企业面试被拒的情况,然后HR会告诉你,虽然没有录用你,但是你进入了他们的人才储备库,那么这个储备库有什么作用和特点呢?我们如何应用人才测评系统完善人才储备库呢? 人才储备一般有以下三…

Python打发无聊时光:12.用PyQt实现简易的心电起搏器界面

第一步:装PyQt库 pip install PyQt5 第二步:复制代码 import sys from PyQt5.QtWidgets import (QApplication, QMainWindow, QPushButton, QVBoxLayout,QWidget, QLabel, QProgressBar, QSlider, QLineEdit, QHBoxLayout) from PyQt5.QtCore import …

软件分层(数据结构/软件逻辑上分层+举例),相连节点的概念+如何相连,为什么是层状结构(软件分层,网络协议分层+梳理协议顺序),协议分层(打电话例子)

目录 软件分层 介绍 举例 类的继承 虚拟文件系统 线程接口封装 虚拟地址空间 总结 为什么是层状的 软件分层 网络协议 原因 梳理协议顺序 相连节点 协议分层 引入 示例 实际上 逻辑上 制定出协议 软件分层 介绍 通过将软件系统划分为不同的层次,每一层都有…

uniApp 调整小程序 单个/全部界面横屏展示效果

我们打开uni项目 小程序端运行 默认是竖着的一个效果 我们打开项目的 pages.json 给需要横屏的界面 的 style 属性 加上 "mp-weixin": {"pageOrientation": "landscape" }界面就横屏了 如果是要所有界面都横屏的话 就直接在pages.json 的 gl…

Ps:海绵工具

海绵工具 Sponge Tool可用于调整图像中特定区域的饱和度,常用于增加或减少颜色的饱和度。 快捷键:O 在特别的灰度图像上,则可用于调整对比度,这可以开发出更多的创意技巧。 ◆ ◆ ◆ 常用操作方法与技巧 1、海绵工具主要用于调整…

源码解析篇 | YOLOv8官方源码项目目录结构解析

前言:Hello大家好,我是小哥谈。YOLOv8是一种目标检测算法,它是YOLO(You Only Look Once)系列算法的第8个版本。YOLOv8相比于之前的版本,在检测精度和速度上都有所提升,它在各种场景下都表现出色…

Git源码管理

参考视频:16-git的日志以及版本管理_哔哩哔哩_bilibili 参考博客:Git && Docker 学习笔记-CSDN博客 目录 简介 个人操作初始化 初始化git目录 查看生成的git目录文件 配置git工作目录的用户信息 查看工作区的状态,生成文件的…

C++面试干货---带你梳理常考的面试题(一)

顾得泉:个人主页 个人专栏:《Linux操作系统》 《C从入门到精通》 《LeedCode刷题》 键盘敲烂,年薪百万! 1.C和C的区别 1.语法和特性:C是一种过程式编程语言,而C是一种面向对象编程语言。C在C的基础上增加…

Java智慧云HIS医院信息化系统源码 更具灵活性、扩展性

目录 什么是云HIS 趋势与转变 HIS上云后有哪些好处 解决方案 云HIS组成 1、门诊挂号 2、住院管理 3、电子病历 4、药物管理 5、统计报表 6、综合维护 7、运营运维 什么是云HIS 云HIS是一种基于云计算技术的医院信息管理系统。云HIS可以帮助医院管理各类医院信息&a…

Linux系统中安装redis+redis后台启动+常见相关配置

1、下载Redis Redis官网:https://redis.io/ 历史版本: http://download.redis.io/releases 2、连接Linux(或者VMwear) 我们安装的是linux版本的redis 打开xftp我们需要先将我们的Redis上传到服务器上 解压到这里 解压的指令 …

Spring MVC源码中设计模式——适配器模式

适配器模式介绍 适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁。这种类型的设计模式属于结构型模式,它结合了两个独立接口的功能。 应用场景: 1、系统需要使用现有的类,而此类的接口不符合系统的需要…

[c++] 继承和多态整理一

1 private 和 protected 继承,子类指针不能赋值给父类指针 如下代码,有一个基类 Base,Derived1,Derived2,Derived3 3 个子类继承了基类 Base,分别是 private 继承,protected 继承,p…

基于springboot+vue的纺织品企业财务管理系统

博主主页:猫头鹰源码 博主简介:Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战,欢迎高校老师\讲师\同行交流合作 ​主要内容:毕业设计(Javaweb项目|小程序|Pyt…