解析CAS算法原理

article/2025/8/30 2:51:06

解析CAS算法原理

    • 什么是CAS?
    • CAS原理
      • 概念
      • 实现形式
      • 底层原理
    • 案例
    • CAS的缺点
    • ABA问题
      • ABA问题如何产生的?
      • 原子的引用
      • 时间戳原子的引用
      • 利用AtomicStampedReference解决ABA问题
        • 案例

什么是CAS?

CAS,全称Compare And Swap,顾名思义,比较再交换。它是一种常用于解决并发编程问题的一种思想。
它是一条CPU并发原语,并且原语的执行过程不允许被打断,不会造成数据不一致问题。它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。

CAS有三个操作数:V内存值、A旧的预期值、B需要修改的值。当且仅当预期值A和内存值V相同时,将内存值V修改为B, 否则什么都不做。

CAS原理

概念

在JUC并发编程的过程中,加锁可以实现线程之间数据同步,但是线程之间对加锁的代码的资源的使用是互斥的,使的运行效率很低;从而引入了volatile关键字,被volatile关键字修饰的变量对所有线程都是共享的,没有了互斥的关系,从而解决了加锁运行效率低的问题,但是volatile关键字不具备原子性,可能会出现同一线程操作同一变量不一致的问题;而CAS思想就可以很好的解决volatile关键字不具备原子性的问题。

实现形式

这是源码对 i++ 自增的实现:
在这里插入图片描述
其通用的实现形式为:

Object var5;
do {
var5 = this.getObjectVolatile(var1, var2);
} while(!this.compareAndSwapObject(var1, var2, var5, var4));
return var5;

var1: 变量对象本身
var2:该对象值的引用地址
var4: 需要改动的数量
var5:通过var1和var2来找出主内存中真实的地址

比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,
否则继续比较直到主内存和工作内存中的值一致为止。

底层原理

CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。

由于Java方法无法直接访问底层系统,需要通过本地(native) 方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因此Java中CAS操作的执行依赖于Unsafe类的方法。

在sun.misc.Unsafe下,我们就以getAndAddInt()方法为例:
在这里插入图片描述
这是一个原子性自增的方法;var2就是通过下面的方法来获取变量var1值的内存地址。然后通过getIntVolatile(var1, var2)方法将该地址下的变量的值复制给var5
在这里插入图片描述
然后在while()中进行条件判断compareAndSwapInt(var1, var2, var5, var4),这个方法就是CAS的典型,比较再交换。将var1变量地址var2下所存放**变量的值(最新值)**与 **var5(旧值)**比较,如果相等,那么var5进行自增;否者再次进入循环,直到旧值与新值一致,也就是再它修改之前没有其他线程将变量修改了,为止。

这就是CAS的思想,通过不断的比较、不断的获取值,以自旋的方式,当旧址与新值相等时,再进行操作,从而保证了操作的原子性。

案例

我们知道volatile关键字不具备原子性,之前我提到AtomicInteger数据类型可以解决这个问题,而AtomicInteger数据类型其实就是利用了CAS思想来解决的,接下来我们来看看CAS是如何解决这一问题的。
如下案例:

 AtomicInteger atomicInteger=new AtomicInteger(5);//System.out.println(atomicInteger.compareAndSet(5, 2019)+"\t current data : "+ atomicInteger.get());//修改成功System.out.println(atomicInteger.compareAndSet(5, 1024)+"\t current data : "+ atomicInteger.get());//修改失败

运行结果如下:
在这里插入图片描述
首先创建一个原子整型变量atomicInteger,初始值为5;接下来是atomicInteger.compareAndSet(5, 2019),我们先看看该方法的源码:
在这里插入图片描述
expect表示期望值,即我们希望他是什么值,或者以为他是什么值。
update表示要修改为什么值。

接着看compareAndSwapInt方法,这是sun.misc包下的Unsafe类里面的方法,该类里面含有大量的native修饰的方法,可以直接操作特定内存的数据,就像cup系统原子指令在执行任务。所以这些方法都不可被打断,从而保证数据的一致性。
在这里插入图片描述
里面有4个参数;

var1: 原子变量本身
var2: 地址偏移量
var4: 期望值
var5: 要修改为的值

前面我们说过地址偏移量是原子变量内部实现valueOffset = unsafe.objectFieldOffset得来的。那么从一开始我们调用的atomicInteger.compareAndSet(5, 2019)方法只传入了期望值和修改值,最后到方法compareAndSwapInt(this,valueOffset ,5, 2019);
最后方法是什么意思呢?

1.根据this(原子变量本身)和地址偏移量(valueOffset )来获取此时主内存中最新的原子变量值;
2.再将最新的原子变量的值与期望值(5)进行比较;
3.如果相等,那么将修改值(2019)赋值给原子变量,并返回true;
4.如果不相等,那么不进行操作,并返回false;

因此,我们再来看这两条打印语句就很清楚了

第一条,期望值是5,因为没有其他线程来修改变量,那么此时真实值就是5,所有将5修改为2019最后返回true;

第二条,期望值也是5,但是前面的操作已经将变量值改为2019,此时从内存获取出来的值就是2019,与期望值5不等,不再进行赋值操作,并返回false。

那么将这种先比较再复制的思想(CAS)与do/while进行结合,就可以保证每一次的操作都是原子操作,并且再期望值与真实值不等的现况下,他会不断的从内存中获取最新值作为期望值,期望值与真实值一致,才能进行接下来的操作。

CAS的缺点

当然CAS也有缺点:

  1. 循环时间长开销大。我们可以看到当getAndAddInt()方法执行时,其中有个do/while,如果CAS一致失败,那么会一直进行尝试。如果长时间的不成功,可能会给CPU带来巨大的开销。

  2. 只能保证一个共享变量的原子性操作。从方法的参数Object var可见,CAS只能对一个共享变量执行原子性操作;对于多个原子性的操作循环CAS就无法保证原子性的操作,这个时候可以用锁来保证原子性。

  3. ABA问题。假如原子变量初始值为A,在获取期望值与获取最新值的期间,有其他线程将原子变量修改为B,然后又修改为A,对于CAS来说,此时期望值与最新值时一致的,所以可以通过,但是其他线程的修改中可能会存在潜在的问题。

ABA问题

ABA问题如何产生的?

CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差内会导致数据的变化。

比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线one操作成功。
尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。

原子的引用

前面的例子中,我们知道AtomicInteger类是对Integer类型进行了原子性封装,当然也对Boolean类进行了封装(AtomicBoolean);那么我想对其他类型或者是自定义类型执行原子性操作应该怎么做呢?原子引用。

通过原子引用AtomicReference可以对其他类型的变量执行原子性操作,如:

//创建一个Integer类型的原子引用atomicReference 
AtomicReference<Integer> atomicReference = new AtomicReference<>( initialValue: 100) ;
//执行CAS操作
atomicReference.compareAndset( expect: 100, update: 101);

其有参构造:
在这里插入图片描述
volatile V value 变量有volatile修饰,其他方法由CAS思想实现,那么就保证了原子性、有序性、可见性。

时间戳原子的引用

时间戳原子的引用AtomicStampedReference<>,顾名思义,带有时间戳的原子引用。
在这里插入图片描述
其有参构造如下:
在这里插入图片描述
initialRef表示要初始化原子引用的值,initialStamp要初始的时间戳;

那么时间戳有什么用呢?———解决ABA问题

利用AtomicStampedReference解决ABA问题

我们知道CAS在执行共享变量的原子性操作时,保证不会受其他线程抢断,但是其他线程可以随意的修改主内存的变量值;那么在CAS获取旧址和获取最新值的期间其他线程可能会操作CAS要操作的变量值,但最终变量值又被改回。

要解决这问题,我们必须知道在那个期间变量到底有没有没其他线程修改过,因此我们对这个变量加了一个新的属性时间戳(可以理解为版本号);每修改了这个变量的值我们就对时间戳+1,如果在那期间有线程对该变量进行了操作那么时间戳就会增加,而执行CAS操作的这个线程执行完后也要对时间戳进行+1,但是我们拿到的时间戳是旧的,所以如果期间没有其他线程进行操作,那么我们自增后的时间戳会比主内存中该变量的时间戳要大,否则就被其他线程修改过。

案例

static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
public static void main(String[] args) {System.out.println("======ABA问题的解决======");new Thread(() -> {int stamp = atomicStampedReference.getStamp();System.out.println(Thread.currentThread().getName() + "\t第一次版本号: " + stamp);try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);System.out.println(Thread.currentThread().getName() + "\t第二次版本号: " + atomicStampedReference.getStamp());atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);System.out.println(Thread.currentThread().getName() + "\t第三次版本号: " + atomicStampedReference.getStamp());}, "t3").start();new Thread(() -> {int stamp = atomicStampedReference.getStamp();System.out.println(Thread.currentThread().getName() + "\t第一次版本号: " + stamp);try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }boolean result=atomicStampedReference.compareAndSet(100,2019,stamp,stamp+1);System.out.println(Thread.currentThread().getName()+"\t修改成功与否:"+result+"  当前最新版本号"+atomicStampedReference.getStamp());System.out.println(Thread.currentThread().getName()+"\t当前实际值:"+atomicStampedReference.getReference());}, "t4").start();}
  1. stamp = atomicStampedReference.getStamp();首先让t3,t4线程获取当前的版本号;在让t3睡1秒,t4睡3秒,以保证获取的版本号为初始版本号以及让t3线程运行结束后t4再运行;
  2. t3先将初始值100改为101,版本号+1为2,再将101改回100,版本号+1为3;
  3. t4线程开始,期望值100与真实值100相等没问题,而版本号(1)+1为2,比真实的版本号3要小,所以不能执行修改,返回false。

运行结果如下:
在这里插入图片描述


http://chatgpt.dhexx.cn/article/8sC5w7wj.shtml

相关文章

深入解析CAS算法原理

目录 一、CAS的基本概念二、CAS算法理解三、CAS开销四、CAS算法在JDK中的应用 一、CAS的基本概念 CAS&#xff1a;Compare and Swap&#xff0c;即比较再交换&#xff0c;是一种硬件对并发的支持&#xff0c;针对多处理器操作而设计的处理器中的一种特殊指令&#xff0c;用于管…

CAS算法实现

https://blog.csdn.net/bluetjs/article/details/52261490?locationNum15&fps1 1.什么是cas&#xff1f; cas是一种无锁算法&#xff08;非阻塞算法&#xff1a;一个线程的失败或者挂起不应该影响其他线程的失败或者&#xff09;&#xff0c;是compare and swap的缩写&am…

修改Idea的jdk版本

概述 idea很多地方都设置了jdk版本&#xff0c;不同模块的jdk版本也可能不一样&#xff0c;下面整理下涉及jdk或者jre版本的几个地方。 方法一 File - Settings - Build, Execution, Deployment - Build Tools - Maven - Importing 方法二 File - Settings - Build, Exec…

Linux切换jdk版本

开发常识 命令行输入下列命令&#xff0c;然后输入 1 、2、3 选择你想要的 jdk 版本&#xff1a; sudo update-alternatives --config java选择完之后查看 jdk 版本&#xff1a; java -version

如何查看JDK版本信息

如何查看JDK版本号 一、前言二、 Windows的dos窗口 一、前言 在下载某些工具时需要知道自己电脑安装的JDK版本号&#xff0c;这里介绍了一种可以看自己JDK版本号的方法。 二、 Windows的dos窗口 1.电脑WindowsR键&#xff0c;打开命令行窗口   2.在命令行里面输入cmd;   3…

mac安装多个JDK版本

因对不同版本的JDK需求&#xff0c;有时候需要安装多个切换使用&#xff0c;这里我为Mac安装了多个JDK。在已有JDK8的基础上又安装了JDK11。 1、国内镜像下载JDK11&#xff0c;下载地址&#xff1a;https://repo.huaweicloud.com/java/jdk/11.0.29/jdk-11.0.2_osx-x64_bin.dmg…

更改JDK版本

1、更改环境变量&#xff1a;JAVA_HOME的路径&#xff08;此路径下要有bin目录&#xff09; 换成你要用的java的版本所在的路径 2、找到系统变量path下的java路径&#xff0c;将此路径下的三个文件全部删掉 3、打开regedit 修改数据数值即可&#xff0c;如下图切换成功

查看javajdk版本

查看当前电脑的Java/JDK版本的方法 1.winR 打开运行窗口&#xff0c;输入 cmd 2.在控制台中输入java --version或者java -version&#xff0c;即可查看Java版本号 Java所有版本 版本号 发布日期 JDK Version 1.0 1996-01-23 Oak(橡树) JDK Version 1.1 1997-02-19 …

如何更换jdk版本

如何更换jdk版本 因为很多时候需要切换jdk版本。也是走了很多弯路才弄好。此次演示的是将1.7 的版本更换成1.8 的。下面是详细步骤先查看当前版本&#xff0c;输入cmd 打开命令提示符后输入 java -version 即可 可以将1.8 的jdk 于1.7 的jdk 安装在同一个目录下&#xff0c;会…

linux 安装多版本jdk

1、先要安装多个版本的jdk&#xff0c;可以从官网进行下载&#xff0c;然后解压到你需要的目录 例如&#xff1a;/home/xxx/Documents/jdk8 /home/xxx/Documents/jdk17 2、先执行软连接设置&#xff0c;将jdk所在的真实路径建立连接 #数字越大默认级别越高sudo updat…

IDEA 切换 JDK 版本

IDEA 中一个项目切换不同的 JDK 版本 File -> Project Structure -> Project -> SDK&#xff1a; IDEA 一个 Project 内&#xff0c;多个 Module 间使用不同的 JDK 问题描述 项目结构如下&#xff1a; 想要在这样一个 Project 中的多个 Module 之间使用不同的 J…

查看 jdk 版本及安装路径

1、查看电脑的 jdk 版本 &#xff08;1&#xff09;键盘 win R 打开 “运行” &#xff0c;输入 cmd 回车&#xff0c;打开命令窗口 &#xff08;2&#xff09;输入 java -version 查看安装的 jdk 版本 2、查看 jdk 的安装路径 &#xff08;1&#xff09;在命令窗口输入 jav…

安装多个jdk版本并切换

官网下载&#xff1a;Java Downloads | Oracle 我们在学习的过程中 经常用到不同的jdk版本 那么如何在一台电脑上同时安装2个jdk版本 并进行切换呢&#xff1f; 我这里面以jdk1.8 和jdk17为例 我已经成功安装2个jdk 一. 查看安装的jdk版本 二 配置 1.配置JAVA_HOME 在系…

更换JDK版本

1.配置环境变量 更换CLASS_PATH指向目录,更换JAVA_HOME指向目录 更换PATH变量中的参数,将jdk与jre指向更换掉 控制台输入java -version 可以观测到版本是否变更 2.IDEA更换jdk配置 需要在idea中选择file--project structure如下图操作更换相关配置 在file--settings中进行下图…

ubuntu切换JDK版本

因为JKD版本的影响&#xff0c;我的ecplise打不开&#xff0c;所以可以采用这种方法切换不同的JDK版本。 首先查看JDK版本&#xff1a; java -version如&#xff1a; 一、安装jdk 我要切换成另外一个版本。如果没有但是有需要的话&#xff0c;可以先安装另外一个版本&#…

安装多个jdk版本

初衷 在安装jdk的过程中由于要和老师教授的jdk版本一致&#xff0c;又不忍心卸载原来的jdk版本。因此想想能不能在一台电脑上安装多个jdk版本&#xff0c;然后无缝切换。在这里记录一下一些步骤与碰到的坑。 期间也查阅了许多博客&#xff0c;在此感谢各位博主。 一. 步骤 1…

宝塔升级JDK版本

宝塔面板 JDK8 → JDK17 一、下载 JDK17 打开服务器命令行&#xff0c;创建并进入/usr/lib/jvm/ 目录&#xff1a; mkdir -p /usr/lib/jvm cd /usr/lib/jvmwget https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.tar.gz二、解压 JDK 安装包并重命名 tar -…

Intellij IDEA--修改JDK版本

原文网址&#xff1a;Intellij IDEA--修改JDK版本_IT利刃出鞘的博客-CSDN博客 简介 本文介绍Idea如何修改JDK的版本。 第1步&#xff1a;配置JDK环境变量 装好JDK之后&#xff0c;要添加一个环境变量&#xff1a;JAVA_HOME&#xff1a; 第2步&#xff1a;修改Idea配置 由Ma…

新版本jdk(9、11、12、13、14)特性

目录 背景 jdk9新特性 目录结构的改变 模块化系统 要解决的问题 概念 实现目标 示例 jShell命令 多版本兼容jar包 接口中的私有方法 钻石操作符(泛型)的升级 try语句的升级 下划线命名标识符的限制 String存储结构的变化 快速创建只读集合 增强的流api takeWhi…

知识小笔记

1. 什么是JDK? JDK有三个版本&#xff0c;分别是&#xff1a; &#xff08;1&#xff09;J2SE: 标准版&#xff0c;主要用于开发桌面应用程序。 &#xff08;2&#xff09;J2EE: 企业版&#xff0c;主要用于开发企业及应用程序&#xff0c;如电子商务网站&#xff0c;ERP系统…