2_CAS 底层原理

概念

CAS 的全称是 Compare-And-Swap,它是 CPU 并发原语

它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的

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


实例

public class CASDemo {
    public static void main(String[] args) {
        // 创建一个原子类
        AtomicInteger atomicInteger = new AtomicInteger(5);

        /**
         * 一个是期望值,一个是更新值,但期望值和原来的值相同时,才能够更改
         * 假设三秒前,我拿的是5,也就是expect为5,然后我需要更新成 2019
         */
        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 atomicInteger = new AtomicInteger(5);

然后调用 CAS 方法,企图更新成 2019,这里有两个参数,一个是 5,表示期望值,第二个就是我们要更新的值

atomicInteger.compareAndSet(5, 2019)

然后再次使用了一个方法,同样将值改成 1024

atomicInteger.compareAndSet(5, 1024)

原因: 这是因为我们执行第一个的时候,期望值和原本值是满足的,因此修改成功,但是第二次后,主内存的值已经修改成了 2019,不满足期望值,因此返回了 false,本次写入失败!!!

在这里插入图片描述


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() 在再和主内存的值进行比较。因为线程不可以直接越过高速缓存,直接操作主内存,所以执行上述方法需要比较一次,在执行加1操作)

那么操作的时候,需要比较工作内存中的值,和主内存中的值进行比较

假设执行 compareAndSwapInt 返回 false,那么就一直执行 while 方法,直到期望的值和真实值一样

  • val1:AtomicInteger对象本身
  • var2:该对象值得引用地址
  • var4:需要变动的数量
  • var5:用var1和var2找到的内存中的真实值
    • 用该对象当前的值与var5比较
    • 如果相同,更新var5 + var4 并返回true
    • 如果不同,继续取值然后再比较,直到更新完成

这里没有用 synchronized,而用 CAS,这样提高了并发性,也能够实现一致性,是因为每个线程进来后,进入的 do while 循环,然后不断的获取内存中的值,判断是否为最新,然后在进行更新操作 。

假设线程 A 和线程 B 同时执行 getAndInt 操作(分别跑在不同的 CPU 上)

  1. AtomicInteger 里面的 value 原始值为 3,即主内存中 AtomicInteger 的 value 为 3,根据JMM 模型,线程 A 和线程 B 各自持有一份价值为 3 的副本,分别存储在各自的工作内存
  2. 线程 A 通过 getIntVolatile(var1 , var2) 拿到 value 值3,这是线程 A 被挂起(该线程失去 CPU 执行权)
  3. 线程 B 也通过 getIntVolatile(var1, var2) 方法获取到 value 值也是3,此时刚好线程 B 没有被挂起,并执行了compareAndSwapInt 方法,比较内存的值也是 3,成功修改内存值为 4,线程B打完收工,一切OK
  4. 这是线程 A 恢复,执行 CAS 方法,比较发现自己手里的数字 3 和主内存中的数字 4 不一致,说明该值已经被其它线程抢先一步修改过了,那么 A 线程本次修改失败,只能够重新读取后在来一遍了,也就是在执行 do while
  5. 线程 A 重新获取 value 值,因为变量 value 被 volatile 修饰,所以其它线程对它的修改,线程 A 总能够看到,线程 A 继续执行 compareAndSwapInt 进行比较替换,直到成功。

Unsafe 类 + CAS 思想: 也就是自旋,自我旋转!!!

补充: 上面说到的 Unsafe 类中的 compareAndSwapInt 是一个本地方法,该方法的实现位于unsafe.cpp 中

  • 先想办法拿到变量 value 在内存中的地址
  • 通过 Atomic::cmpxchg 实现比较替换,其中参数 X 是即将更新的值,参数 e 是原内存的值

CAS 存在的问题

CAS 是一种乐观锁,它避免了悲观锁独占锁对象的情况,同时也提高了并发性能

存在问题如下:

  • 乐观锁只能保证一个共享变量的原子操作。 如果存在多个变量,乐观锁将显得力不从心【但互斥锁能轻易解决,不管对象数量的多少级对象颗粒的大小】

  • 长时间自旋可能导致开销大。 加入 CAS 长时间操作不成功一直自旋,会给 CPU带来很大的开销。

  • ABA 问题 。 CAS 的核心思想是通过比较内存值和预期值是否一样而判断内存值是否被更改过,但此判断逻辑不严谨,假如内存值为 A,后来一条线程修改为 B,最后又被另一个线程改成了 A,则 CAS 认为内存值并没有发生过改变,但实际情况是有被其他线程修改,这种情况对依赖过程值的情景的运算结果影响很大。

    解决办法:引入版本号,每次变量更新都把版本号【时间戳】加一。


小总结

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

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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值