2023.02.14 CAS学习

CAS(CompareAndSwap)是一种无锁算法,用于保证线程安全。它通过比较内存中的值并交换,以实现原子性的更新。Java中的CAS使用JNI调用底层CPU指令实现,例如AtomicInteger类就是其应用示例。CAS避免了锁的开销,但存在ABA问题,即值改变后再变回原值可能导致错误判断。为解决这个问题,可以引入版本号机制。
摘要由CSDN通过智能技术生成

CAS机制是什么?

CAS 就是 Compare And Swap,意为 比较并交换值。
CAS机制给线程安全提供了新的保障思路,之前通过加锁的方式’将指令打包成一个整体’(指的某sync锁),来实现线程安全。CAS出现之后可以不进行加锁也能保证线程的安全。

从代码角度来看:当多个线程对一个资源进行CAS(比较并交换值)操作时,CAS机制保证了只能有一个线程操作成功,并且不会阻塞其余的线程,其余线程只会收到操作失败的提示。具体的操作步骤下面会有说明。

CAS 是乐观锁的一种具体的实现。

CAS如何保证线程安全?

Java中的 CAS 机制是基于 JNI 调用 C 给 CPU 上锁实现线程安全的,具体过程可以参考数据库。
JNI:Java Navicat Interface,JNI可以理解为是一种协议或者规范。
JNI 相当于 Java 和 C 之间的桥梁,通过 JNI 协议可以让 Java调用 C,可以通过协议让 C 调用 Java。
Java 语言就是通过 JNI 实现了跨语言调用。例如:Java调用C脚本(Navicat修饰的方法)。

CAS的原理:

123 :必须前两个值相等才可以修改
112 可以修改
123 不可以修改
132 不可以修改
第一位:内存中的值(主存)
第二位:旧值
第三位:新值

由于Java语言是跨平台语言,所以不同的平台有不同的CAS实现原理:

  • java的CAS使用了 Unsafe 这类进行 cas操作。
  • Unsafe类 通过 jvm对不同操作系统实现的 Atomic::cmpxchg 指令实现。
  • Atomic::cmpxchg指令是由 汇编语言 实现的,并且使用了CPU硬件的lock机制保证了 指令操作的 原子性。
    所以 CAS机制可以保证线程安全是因为有了硬件的支持,才能在软件做到不加锁 的线程安全。

Unsafe类通过静态方法获取

private static final Unsafe unsafe = Unsafe.getUnsafe();

原子类(AtomicInteger等)

java.util.concurrent.atomic.AtomicInteger。
atomic包下还有很多类型的原子类,java.util.concurrent 就是常说的JUC编程。

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // 声明的属性
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private volatile int value;
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    
    //其中的几个方法
    public final int get() {return value;}
    
    public final void set(int newValue) {value = newValue; }
    
    //将当前value自增1
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

}

参数理解:

  • unsafe:调用CAS操作的工具类。
  • value:对于AtomicInteger类来说 保存的就是 Integer类型的值。
  • valueOffset:value值对应的内存中编译的地址,这样也是为了能够能快的在内存中找到value值。

CAS操作的过程

使用CAS机制同时启动两个线程并且对一个资源进行自增操作。

private static AtomicInteger atomicInteger = new AtomicInteger(0);

    public static void main(String[] args) throws Exception {

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                atomicInteger.getAndIncrement();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                atomicInteger.getAndIncrement();
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(atomicInteger.get());
    }
    
==================================结果==================================
D:\JDK\jdk-8u291\bin\java.exe "-javaagent:D:\idea\IntelliJ IDEA 2021.2\lib\idea_rt.jar=59466:D:\idea\IntelliJ IDEA 2021.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JDK\jdk-8u291\jre\lib\charsets.jar;D:\JDK\jdk-8u291\jre\lib\deploy.jar;D:\JDK\jdk-8u291\jre\lib\ext\access-bridge-64.jar;D:\JDK\jdk-8u291\jre\lib\ext\cldrdata.jar;D:\JDK\jdk-8u291\jre\lib\ext\dnsns.jar;D:\JDK\jdk-8u291\jre\lib\ext\jaccess.jar;D:\JDK\jdk-8u291\jre\lib\ext\jfxrt.jar;D:\JDK\jdk-8u291\jre\lib\ext\localedata.jar;D:\JDK\jdk-8u291\jre\lib\ext\nashorn.jar;D:\JDK\jdk-8u291\jre\lib\ext\sunec.jar;D:\JDK\jdk-8u291\jre\lib\ext\sunjce_provider.jar;D:\JDK\jdk-8u291\jre\lib\ext\sunmscapi.jar;D:\JDK\jdk-8u291\jre\lib\ext\sunpkcs11.jar;D:\JDK\jdk-8u291\jre\lib\ext\zipfs.jar;D:\JDK\jdk-8u291\jre\lib\javaws.jar;D:\JDK\jdk-8u291\jre\lib\jce.jar;D:\JDK\jdk-8u291\jre\lib\jfr.jar;D:\JDK\jdk-8u291\jre\lib\jfxswt.jar;D:\JDK\jdk-8u291\jre\lib\jsse.jar;D:\JDK\jdk-8u291\jre\lib\management-agent.jar;D:\JDK\jdk-8u291\jre\lib\plugin.jar;D:\JDK\jdk-8u291\jre\lib\resources.jar;D:\JDK\jdk-8u291\jre\lib\rt.jar;E:\idea-peoject\thread-demo\out\production\thread-demo com.lhj.juc.juc_atomicIntegerDemo_01
100000

Process finished with exit code 0

CAS的执行步骤

前提对虚拟机、内存结构有一定了解。
题外话:
每次创建一个线程就会在虚拟机中创建一个虚拟机栈,线程的生命周期跟虚拟机栈的生命周期相同。
对象都存放在主内存(主存)也就是堆中,线程操作主存中的变量时,会将主存中变量值的副本复制一份,放到线程本地。线程本地的副本指向堆中的变量(栈指向堆)。

  • 1)有线程1和线程2将 value 的值读取到 线程本地的 oldValud 中(oldValue 代表一个局部变量, 在栈上。每个线程有自己的栈)

在这里插入图片描述

  • 2)线程2先执行操作 ,线程2判断 oldValue 等于 value 等于 0,直接对 value 赋值。操作成功后oldValue 同步 value 的值。
    • CAS操作是直接操作主内存(主存)的,而不是操作寄存器(线程本地的变量副本)
    • CAS的 读、比较、写 的操作 是一条硬件指令,是原子的。
      注:感兴趣的话可以去看看 CPU的 “总线锁定”

在这里插入图片描述
直接对 value 赋值。
在这里插入图片描述
操作成功后 线程2 oldValue 同步 value 的值。
在这里插入图片描述

3)此时线程1 执行 CAS操作,oldValue 和 value 不相等, 不能进行赋值。
在这里插入图片描述
线程1会进入循环,在循环里重新读取 value 的值赋给 oldValue
在这里插入图片描述
线程1进行第二次执行 CAS, 此时 oldValue 和 value 相同, 于是直接执行赋值操作
在这里插入图片描述
通过形如上述代码就可以实现一个原子类。不需要使用重量级锁,就可以高效的完成多线程的自增操作。

CAS的ABA问题

ABA问题是指:
在这里插入图片描述
线程A和B同时共享一个资源变量
线程B想要修改value值,需要经过两个过程:

  • 先读取 value 的值, 记录到 oldNum 变量中.
  • 使用 CAS 判定当前 value 的值是否为 A, 如果为 A, 就修改成 其他值
    但是在线程B执行这两个操作之间,线程A可以将value 进行修改,并且最终改回原来的值。
    此时线程B无法判定 value的是 是初始值,还是经过其他线程运算后的值。

大部分情况下 线程改变了值,最终改回去 不会出现问题。但是不排除一些特殊情况:

正常的过程

1)存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期望更新为 50.
2)线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中.
3)轮到线程2 执行了, 发现当前存款为 50, 和之前读到的 100 不相同, 执行失败.

异常的过程

1)存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期望更新为 50
2)线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中。
3)在线程2 执行之前, 滑稽的朋友正好给滑稽转账 50, 账户余额变成 100!!!
4)轮到线程2 执行了, 发现当前存款为 100, 和之前读到的 100 相同, 再次执行扣款操作这个时候, 扣款操作被执行了两次!!! 都是 ABA 问题搞的鬼
解决ABA问题:
在这里插入图片描述
给value 增加一个属性 version ,每次有线程成功执行CAS操作之后 就给version自增1。通过版本号线程就可以判断value是否被修改过。

参考:
CAS详解
CAS原子操作
CAS

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值