写点什么

CAS 原理,看这一篇就够了!

  • 2023-12-08
    湖南
  • 本文字数:2806 字

    阅读完需:约 9 分钟

CAS原理,看这一篇就够了!

一、核心思想

CAS 涉及到以下操作:

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

1、比较 A 与 V 是否相等。(比较)

2、如果比较相等,将 B 写入 V。(交换)

3、返回操作是否成功


二、CAS 基础实现

CAS 是 compare and swap 的简写,即比较并交换。它是指一种操作机制,而不是某个具体的类或方法。在 Java 平台上对这种操作进行了包装。在 Unsafe 类中,调用代码如下:

unsafe.compareAndSwapInt(this, valueOffset, expect, update);
复制代码

它需要三个参数,分别是内存位置 V,旧的预期值 A 和新的值 B。

  1. 操作时,先从内存位置读取到值,然后和预期值 A 比较。

  2. 如果相等,则将此内存位置的值改为新值 B,返回 true。

  3. 如果不相等,说明和其他线程冲突了,则不做任何改变,返回 false。

这种机制在不阻塞其他线程的情况下避免了并发冲突,比独占锁的性能高很多。 CAS 在 Java 的原子类和并发包中有大量使用。

三、重试机制(循环 CAS)

有很多文章说,CAS 操作失败后会一直重试直到成功,这种说法很不严谨。

第一,CAS 本身并未实现失败后的处理机制,它只负责返回成功或失败的布尔值,后续由调用者自行处理。只不过我们最常用的处理方式是重试而已。

第二,这句话很容易理解错,被理解成重新比较并交换。实际上失败的时候,原值已经被修改,如果不更改期望值,再怎么比较都会失败。而新值同样需要修改。

所以正确的方法是,使用一个死循环进行 CAS 操作,成功了就结束循环返回,失败了就重新从内存读取值和计算新值,再调用 CAS。看下 AtomicInteger 的源码就什么都懂了:


在 AtomicInteger 数据定义的部分,我们可以看到,其实实际存储的值是放在 value 中的,除此之外我们还获取了 unsafe 实例,并且定义了 valueOffset。

再看到 static 块,懂类加载过程的都知道,static 块的加载发生于类加载的时候,是最先初始化的,这时候我们调用 unsafe 的 objectFieldOffset 从 Atomic 类文件中获取 value 的偏移量,那么 valueOffset 其实就是记录 value 的偏移量的。


再回到上面一个函数 getAndAddInt,我们看 var5 获取的是什么,通过调用 unsafe 的 getIntVolatile(var1, var2),这是个 native 方法,具体实现到 JDK 源码里去看了,其实就是获取 var1 中,var2 偏移量处的值。var1 就是 AtomicInteger,var2 就是我们前面提到的 valueOffset,这样我们就从内存里获取到现在 valueOffset 处的值了。

现在重点来了,compareAndSwapInt(var1, var2, var5, var5 + var4)其实换成 compareAndSwapInt(obj, offset, expect, update)比较清楚:

  1. 如果 obj 内的 value 和 expect 相等,就证明没有其他线程改变过这个变量,那么就更新它为 update;

  2. 如果这一步的 CAS 没有成功,那就采用自旋的方式继续进行 CAS 操作;

其实这两个步骤,在 JNI 里是借助于一个 CPU 指令完成的。所以还是原子操作。

四、核心原理

CAS 底层使用 JNI 调用 C 代码实现,如果你有 Hotspot 源码,那么在 Unsafe.cpp 里可以找到实现:


我们可以看到 compareAndSwapInt 实现是在 Unsafe_CompareAndSwapInt 里面,再深入到 Unsafe_CompareAndSwapInt:


p 是取出的对象,addr 是 p 中 offset 处的地址,最后调用了 Atomic::cmpxchg(x, addr, e),其中参数 x 是即将更新的值,参数 e 是原内存的值。代码中能看到 cmpxchg 有基于各个平台的实现,这里我选择 Linux X86 平台下的源码分析:


这是一段小汇编,__asm__说明是 ASM 汇编,__volatile__禁止编译器优化:

// Adding a lock prefix to an instruction on MP machine#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "
复制代码

os::is_MP 判断当前系统是否为多核系统,如果是就给总线加锁,所以同一芯片上的其他处理器就暂时不能通过总线访问内存,保证了该指令在多处理器环境下的原子性。

在正式解读这段汇编前,我们来了解下嵌入汇编的基本格式:

asm ( assembler template    : output operands                  /* optional */    : input operands                   /* optional */    : list of clobbered registers      /* optional */    );
复制代码
  • template:就是 cmpxchgl %1,(%3)表示汇编模板

  • output operands:表示输出操作数,=a 对应 eax 寄存器

  • input operand:表示输入参数,%1 就是 exchange_value, %3 是 dest, %4 就是 mp, r 表示任意寄存器,a 还是 eax 寄存器

  • list of clobbered registers:就是些额外参数,cc 表示编译器 cmpxchgl 的执行将影响到标志寄存器, memory 告诉编译器要重新从内存中读取变量的最新值,这点实现了 volatile 的感觉。

那么表达式其实就是 cmpxchgl exchange_value ,dest,我们会发现 %2 也就是 compare_value 没有用上,这里就要分析 cmpxchgl 的语义了。cmpxchgl 末尾 l 表示操作数长度为 4,上面已经知道了。cmpxchgl 会默认比较 eax 寄存器的值即 compare_value 和 exchange_value 的值,如果相等,就把 dest 的值赋值给 exchange_value,否则,将 exchange_value 赋值给 eax。具体汇编指令可以查看 Intel 手册 CMPXCHG

最终,JDK 通过 CPU 的 cmpxchgl 指令的支持,实现 AtomicInteger 的 CAS 操作的原子性。


五、底层实现

CAS 底层是靠调用 CPU 指令集的 cmpxchg 完成的,它是 x86 和 Intel 架构中的 compare and exchange 指令。在多核的情况下,这个指令也不能保证原子性,需要在前面加上 lock 指令。

lock 指令可以保证一个 CPU 核心在操作期间独占一片内存区域。那么 这又是如何实现的呢?

在处理器中,一般有两种方式来实现上述效果:总线锁和缓存锁。在多核处理器的结构中,CPU 核心并不能直接访问内存,而是统一通过一条总线访问。

  • 总线锁就是锁住这条总线,使其他核心无法访问内存。这种方式代价太大了,会导致其他核心停止工作。

  • 缓存锁并不锁定总线,只是锁定某部分内存区域。当一个 CPU 核心将内存区域的数据读取到自己的缓存区后,它会锁定缓存对应的内存区域。锁住期间,其他核心无法操作这块内存区域。

CAS 就是通过这种方式实现比较和交换操作的原子性的。值得注意的是, CAS 只是保证了操作的原子性,并不保证变量的可见性,因此变量需要加上 volatile 关键字。

六、CAS 的问题

1、ABA 问题

CAS 需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新。但是如果一个值原来是 A,变成了 B,又变成了 A,那么使用 CAS 进行检查时会发现它的值没有发生变化,但是实际上却变化了。这就是 CAS 的 ABA 问题。

常见的解决思路是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么 A-B-A 就会变成 1A-2B-3A。

目前在 JDK 的 atomic 包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。这个类的 compareAndSet 方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

2、循环时间长开销大

如果 CAS 不成功,则会原地自旋,如果长时间自旋会给 CPU 带来非常大的执行开销。

最后

LZ整理了一整套2023年最新的面试资料,如果有需要的小伙伴点击此处即可~


转载

作者:summer_west_fish

原文链接:https://blog.csdn.net/summer_fish/article/details/124980876

用户头像

java技术来一打~ 2023-12-01 加入

还未添加个人简介

评论

发布
暂无评论
CAS原理,看这一篇就够了!_CAS_是月月啊2023_InfoQ写作社区