CAS详解

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

一、CAS概念

1.1 CAS是什么

 Compare And Swap

比较并交换

1. 如果线程的期望值跟物理内存的真实值一样,就更新值到物理内存当中,并返回true

2. 如果线程的期望值跟物理内存的真实值不一样,返回false,那么本次修改失败,那么此时需要重新获得主物理内存的新值

1.2 图解说明

主物理内存有一个共享变量值为5,有两个线程T1,T2,他们都有自己的工作内存,并且有变量的拷贝(快照5),T1线程现在把值改为2019,然后,写回主物理内存并通知其它线程可见(加volatile),这个过程中。T1的期望值为5,要跟主物理内存的值5进行对比,如果相同,说明没有其它线程改变,则将主物理内存的值改为2019,并返回true。这时,T2线程也将自己工作内存的值改为了1024,但是,当写回主物理内存时,发现自己的期望值(5),与现在的主物理内存(2019)不一样了,就会写入失败,返回false,此时需要重新获得主物理内存的新值

1.3 代码示例 

/*** CAS是什么? ->compare and swap* 比较并交换*/
public class CASDemo {public static void main(String[] args) {AtomicInteger atomicInteger = new AtomicInteger(5);//模拟T1线程System.out.println(atomicInteger.compareAndSet(5, 2019) + "\t current data: " + atomicInteger.get());//模拟T2线程System.out.println(atomicInteger.compareAndSet(5, 1024) + "\t current data: " + atomicInteger.get());}
}

二、CAS底层原理

2.1 UnSafe类

2.1.1 Unsafe

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

注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法,都直接调用操作系统底层资源执行相应任务

2.1.2 valueOffset

变量valueOffSet,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的

当前对象,这个地址的值,进行加1操作

2.1.3 value

变量value用volatile修饰,保证了多线程之间的内存可见性

2.2 CAS是什么

2.2.1 深层分析

CAS的全称为Compare-And-Swap,它是一条CPU并发原语。它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的

CAS并发原语体现在java语言中就是sun.misc.Unsafe类中的各个方法。调用Unsafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题

  1. var1:当前对象,AtomicInteger对象本身
  2. var2: 该对象值的引用地址
  3. var4:需要变动的数值
  4. var5:是用var1和var2找出的主内存中真实的值

用该对象当前的值与var5主物理内存的值比较

如果相同,更新var5+var4,并且返回true

如果不同,继续取值然后再比较,直到更新完成

synchronized加锁,同一时间,只能有一个线程访问,一致性得到了保障,并发性下降。CAS用的do while,没有加锁,反复的通过CAS比较,直到成功,既保证了一致性,又提高了并发性。

原子整型之所以在i++这种多线程的环境下面,不用加synchronized,就凭着底层Unsafe类也能来保证原子性,来保证线程安全,是因为Unsafe是CAS的核心类,CAS是比较并交换,Unsafe类根据内存偏移地址,来获取数据。

2.2.2 流程分析

 

假设线程A和线程B两个线程同时执行getAndAddInt操作(分别在不同CPU上)

1. AutomicInteger里面的value原始值为5,即主内存中AtomicInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为5的value的副本分别到各自的工作内存

2. 线程A通过getIntVolatile(var1,var2)拿到value值5,这时线程A被挂起

3.线程B也通过getIntVolatile(var1,var2)拿到value值5,此时刚好线程B没有被挂起,并执行compareAndSwapInt方法比较内存中的值也是5,成功修改内存的值为2019,线程B打完收工一切OK

4. 这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的数值和内存中的数字2019不一致,说明该值已经被其它线程抢先一步修改了,那A线程修改失败,只能重新来一遍了

5.线程A重新获取value值,因为变量value是volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt方法进行比较替换,直到成功

2.2.3 底层汇编

2.2.4 小结

1. CAS

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

2. CAS应用

CAS有3个操作数,内存值V,旧的预期值A,要修改的更新值B。

当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

三、CAS缺点

3.1 多次比较循环时间长开销很大

我们可以看到getAndAddInt方法执行时,有个do while

如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。

3.2 只能保证一个共享变量的原子性

CAS仅针对当前对象

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是,对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。

3.3 引入ABA问题

3.3.1 ABA问题的产生

两个线程T1和T2,T1完成要10s,T2完成要2s,现在主内存有一个值A,T1和T2各有一个拷贝,T2因为很快,先把值更新为B,然后,又把主内存的B更新为A,这时候,T1线程准备更新值时,与主内存值比较发现还是A,认为没有变化。但是,由于时间差的问题,这个值可能已经被其他线程改了多次,只不过最后还是改成了原来的A,这就是ABA问题。

3.3.2 原子引用

CAS会导致“ABA问题”

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

比如说一个线程one,从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后

线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。

尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。

@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
class User {private String name;private int age;
}
public class AtomicReferenceDemo {public static void main(String[] args) {User a = new User("a", 22);User b = new User("b", 23);AtomicReference<User> atomicReference = new AtomicReference<>();atomicReference.set(a);System.out.println(atomicReference.compareAndSet(a, b) + "\t" + atomicReference.get().toString());System.out.println(atomicReference.compareAndSet(a, b) + "\t" + atomicReference.get().toString());}
}

3.3.3 时间戳原子引用

*** ABA问题的解决 AtomicStampedReference*/
public class ABADemo {static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);//初始化值和版本号static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);public static void main(String[] args) {System.out.println("***********************以下是ABA问题的产生**********************");new Thread(() -> {atomicReference.compareAndSet(100, 101); //100 101 100 即ABA问题,先改为101,然后再改为100atomicReference.compareAndSet(101, 100);}, "t1").start();new Thread(() -> {//暂停1秒钟t2线程,保证上面的t1线程完成了一次ABA操作try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());}, "t2").start();//暂停一会儿线程try {TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("***********************以下是ABA问题的解决**********************");new Thread(() -> {int stamp = atomicStampedReference.getStamp();System.out.println(Thread.currentThread().getName() + "\t第1次版本号: " + stamp);//暂停1秒钟t3线程,让t4线程获取相同的版本号,同一起点try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}//如果期望的值是100,版本号是atomicStampedReference.getStamp(),就让版本号加1,值改为101atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);System.out.println(Thread.currentThread().getName() + "\t第2次版本号: " + atomicStampedReference.getStamp());atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);System.out.println(Thread.currentThread().getName() + "\t第3次版本号: " + atomicStampedReference.getStamp());}, "t3").start();new Thread(() -> {int stamp = atomicStampedReference.getStamp();System.out.println(Thread.currentThread().getName() + "\t第1次版本号: " + stamp);//暂停3秒钟t4线程,保证上面的t3线程完成了一次ABA操作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 + "\t当前最新实际版本号: " + atomicStampedReference.getStamp());System.out.println(Thread.currentThread().getName() + "\t当前实际最新值: " + atomicStampedReference.getReference());}, "t4").start();}
}

规避ABA,解决ABA问题

就是新增一种机制,那就是增加时间戳,当时间戳跟要对比的时间戳不一致的话,就说明这个数据在中间被修改过,类似于乐观锁版本号。

3.4 总结

synchronized加锁,一致性保证,并发性下降

CAS不加锁,保证一致性,但是它如果多次比较,耗时时间长,开销大。

视频教程,源码


http://chatgpt.dhexx.cn/article/5f9V64HW.shtml

相关文章

CAS算法的理解及应用

应用 原子操作类&#xff0c;例如AtomicInteger&#xff0c;AtomicBoolean …适用于并发量较小&#xff0c;多cpu情况下&#xff1b; Java中有许多线程安全类&#xff0c;比如线程安全的集合类。从Java5开始&#xff0c;在java.util.concurrent包下提供了大量支持高效并发访问…

解析CAS算法原理

解析CAS算法原理 什么是CAS&#xff1f;CAS原理概念实现形式底层原理 案例CAS的缺点ABA问题ABA问题如何产生的&#xff1f;原子的引用时间戳原子的引用利用AtomicStampedReference解决ABA问题案例 什么是CAS&#xff1f; CAS&#xff0c;全称Compare And Swap&#xff0c;顾名…

深入解析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…