CAS 底层原理
概念
CAS 的全称是 Compare-And-Swap, 它是 CPU 并发原语
它的功能是判断内存某个位置的值是否为预期值, 如果是则更改为新的值, 这个过程是原子的
CAS 并发原语体现在 Java 语言中就是 sun.misc.Unsafe 类的各个方法. 调用 UnSafe 类中的 CAS 方法, JVM 会帮我们实现出 CAS 汇编指令, 这是一种完全依赖于硬件的功能, 通过它实现了原子操作, 再次强调, 由于 CAS 是一种系统原语, 原语属于操作系统应用范畴, 是由若干条指令组成, 用于完成某个功能的一个过程, 并且原语的执行必须是连续的, 在执行过程中不允许被中断, 也就是说 CAS 是一条 CPU 的原子指令, 不会造成所谓的数据不一致的问题, 也就是说 CAS 是线程安全的.
代码使用
首先调用 AtomicInteger 创建了一个实例, 并初始化为 10
- // 创建一个原子类
- AtomicInteger atomicInteger = new AtomicInteger(10);
然后调用 CAS 方法, 企图更新成 2020, 这里有两个参数, 一个是 10, 表示期望值, 第二个就是我们要更新的值
atomicInteger.compareAndSet(10, 2020)
然后再次使用了一个方法, 同样将值改成 2021
atomicInteger.compareAndSet(10, 2021)
完整代码如下:
- /**
- * CAS 比较并交换
- * @author xiao
- * @date 2020/4/23 9:14
- */
- public class CASDemo {
- public static void main(String[] args) {
- AtomicInteger atomicInteger = new AtomicInteger(10);
- /**
- * 一个是期望值, 一个是更新值, 但期望值和原来的值相同时, 才能够更改
- * 假设拿的是 10, 也就是 expect 为 5, 然后我需要更新成 2020
- */
- System.out.println(atomicInteger.compareAndSet(10, 2020)+"\t 当前的值为:"+atomicInteger.get());
- System.out.println(atomicInteger.compareAndSet(10, 2021)+"\t 当前的值为:"+atomicInteger.get());
- }
- }
上面代码的执行结果为
这是因为我们执行第一个的时候, 期望值和原本值是满足的, 因此修改成功, 但是第二次后, 主内存的值已经修改成了 2020, 不满足期望值, 因此返回了 false, 本次写入失败
这个就类似于 SVN 或者 Git 的版本号, 如果没有人更改过, 就能够正常提交, 否者需要先将代码 pull 下来, 合并代码后, 然后提交
CAS 底层原理
首先我们先看看 atomicInteger.getAndIncrement()方法的源码
从这里能够看到, 底层又调用了一个 unsafe 类的 getAndAddInt 方法
1,unsafe 类
Unsafe 是 CAS 的核心类, 由于 Java 方法无法直接访问底层系统, 需要通过本地 (Native) 方法来访问, Unsafe 相当于一个后门, 基于该类可以直接操作特定的内存数据. Unsafe 类存在 sun.misc 包中, 其内部方法操作可以像 C 的指针一样直接操作内存, 因为 Java 中的 CAS 操作的执行依赖于 Unsafe 类的方法.
注意 Unsafe 类的所有方法都是 native 修饰的, 也就是说 unsafe 类中的方法都直接调用操作系统底层资源执行相应的任务
为什么 Atomic 修饰的包装类, 能够保证原子性, 依靠的就是底层的 unsafe 类
2, 变量 valueOffset
表示该变量值在内存中的偏移地址, 因为 Unsafe 就是根据内存偏移地址获取数据的.
从这里我们能够看到, 通过 valueOffset, 直接通过内存地址, 获取到值, 然后进行加 1 的操作
3, 变量 value 用 volatile 修饰
保证了多线程之间的内存可见性
var5: 就是我们从主内存中拷贝到工作内存中的值
那么操作的时候, 需要比较工作内存中的值, 和主内存中的值进行比较
假设执行 compareAndSwapInt 返回 false, 那么就一直执行 while 方法, 直到期望的值和真实值一样
val1:AtomicInteger 对象本身
var2: 该对象值得引用地址
var4: 需要变动的数量
var5: 用 var1 和 var2 找到的内存中的真实值
用该对象当前的值与 var5 比较
如果相同, 更新 var5 + var4 并返回 true
如果不同, 继续取值然后再比较, 直到更新完成
这里没有用 synchronized, 而用 CAS, 这样提高了并发性, 也能够实现一致性, 是因为每个线程进来后, 进入的 do while 循环, 然后不断的获取内存中的值, 判断是否为最新, 然后在进行更新操作.
假设线程 A 和线程 B 同时执行 getAndInt 操作(分别跑在不同的 CPU 上)
AtomicInteger 里面的 value 原始值为 3, 即主内存中 AtomicInteger 的 value 为 3, 根据 JMM 模型, 线程 A 和线程 B 各自持有一份价值为 3 的副本, 分别存储在各自的工作内存
线程 A 通过 getIntVolatile(var1 , var2) 拿到 value 值 3, 这是线程 A 被挂起(该线程失去 CPU 执行权)
线程 B 也通过 getIntVolatile(var1, var2)方法获取到 value 值也是 3, 此时刚好线程 B 没有被挂起, 并执行了 compareAndSwapInt 方法, 比较内存的值也是 3, 成功修改内存值为 4, 线程 B 打完收工, 一切 OK
这是线程 A 恢复, 执行 CAS 方法, 比较发现自己手里的数字 3 和主内存中的数字 4 不一致, 说明该值已经被其它线程抢先一步修改过了, 那么 A 线程本次修改失败, 只能够重新读取后在来一遍了, 也就是在执行 do while
线程 A 重新获取 value 值, 因为变量 value 被 volatile 修饰, 所以其它线程对它的修改, 线程 A 总能够看到, 线程 A 继续执行 compareAndSwapInt 进行比较替换, 直到成功.
Unsafe 类 + CAS 思想: 也就是自旋, 自我旋转
底层汇编
Unsafe 类中的 compareAndSwapInt 是一个本地方法, 该方法的实现位于 unsafe.cpp 中
先想办法拿到变量 value 在内存中的地址
通过 Atomic::cmpxchg 实现比较替换, 其中参数 X 是即将更新的值, 参数 e 是原内存的值
CAS 缺点
CAS 不加锁, 保证一次性, 但是需要多次比较
循环时间长, 开销大(因为执行的是 do while, 如果比较不成功一直在循环, 最差的情况, 就是某个线程一直取到的值和预期值都不一样, 这样就会无限循环)
只能保证一个共享变量的原子操作
当对一个共享变量执行操作时, 我们可以通过循环 CAS 的方式来保证原子操作
但是对于多个共享变量操作时, 循环 CAS 就无法保证操作的原子性, 这个时候只能用锁来保证原子性
引出来 ABA 问题?
ABA 问题
总结
CAS
CAS 是 compareAndSwap, 比较当前工作内存中的值和主物理内存中的值, 如果相同则执行规定操作, 否者继续比较直到主内存和工作内存的值一致为止
CAS 应用
CAS 有 3 个操作数, 内存值 V, 旧的预期值 A, 要修改的更新值 B. 当且仅当预期值 A 和内存值 V 相同时, 将内存值 V 修改为 B, 否则不操作
来源: https://www.cnblogs.com/bbgs-xc/p/12758579.html