CAS是什么?

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

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

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

一个小案例:

package com.juc.cas;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * Created by 乐心湖 on 2020/5/3 15:04
 */
public class CASDemo {
    public static void main(String[] args) {
        TestCAS();
    }

    public static void TestCAS(){
        AtomicInteger atomicInteger = new AtomicInteger(5);

        System.out.println(atomicInteger.compareAndSet(5, 2020) + "\t current data is " + atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(5, 2001) + "\t current data is " + atomicInteger.get());
    }
}
true     current data is 2020
false     current data is 2020

首先,AtomicInteger初始值为5

第一个线程开始,要跟内存比较并交换,线程的期望值为5 ,而刚好内存值就是5, 然后交换了值,也就是把主物理内存的值5改为了2019,然后返回了个true代表取到的值与期望值是一样的

然后通知其他线程可见了,第二个线程来了,发现主物理内存是2019,跟自己的期望值5不一样啊,然后就返回了个false,主物理内存并没有改变~

CAS底层原理

为什么这里不加synchronized也能在多线程下保持线程安全呢

我们点进去查看核心源码

用来比较

public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

实现+1

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

你可以看到最后一个参数为1,也就是它底层做的是类似于i++的操作

  • this,当前对象
  • 变量ValueOffset,是该变量在内存中的偏移地址,因为UnSafe就是根据内存偏移地址获取数据的

这个非常关键的类unsafe,Unsafe是CAS的核心类

Unsafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native) 方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。

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

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

具体的实现逻辑,一个循环,调用上面 比较+1

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    return var5;
}

var1 AtomicInteger对象本身

var2 该对象的偏移地址

var4 就是传进来的1

var5 通过var1,var2找出的主内存中真实的值,用该对象前的值与var5比较;

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

如果不同,继续循环获取真实值即value变量,由于它被volatile修饰,其他线程对他的修改都是可以互相看到的,然后再继续比较,直到更新完成。

更简单的说,这个实现的逻辑是这样的:

首先, var1代表当前对象,var2代表对象的偏移地址,var4就是那个+1的值,然后getIntVolatile()这个方法去获取当前对象的这个值是多少,给他保存到var5,然后,compareAndSwapInt()这个方法去再比较当前对象的值还是不是var5,是的话返回true,因为条件!,所以在while里面就是false就退出循环,最后返回出+1后的值。如果当前对象不是之前的var5了,返回一个false,while循环里面就是true,继续循环,拿到下一个值去比较,直到比较成功~

JMM模型分析

我们再来理解一遍,

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

CAS缺点

  • 循环时间长,开销大

    例如getAndAddInt方法执行,有个do while循环,如果CAS失败,一直会进行尝试,如果CAS长时间不成功,可能会给CPU带来很大的开销

  • 只能保证一个共享变量的原子操作

    对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性

  • ABA问题

ABA问题如何产生

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

比如线程1从内存位置V取出A,线程2同时也从内存取出A,并且线程2进行一些操作将值改为B,然后线程2又将V位置数据改成A,这时候线程1进行CAS操作发现内存中的值依然时A,然后线程1操作成功。

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

如何解决这一问题呢,下面我们去了解两种方式。

原子引用

将属性值封装起来,使用AtomicReference泛型

@AllArgsConstructor
@NoArgsConstructor
@Data
class User{
    String username;
    int age;
}
public class AtomicReferenceDemo {
    public static void main(String[] args) {
        User u1 = new User("我是u1",12);
        User u2 = new User("我是u2",15);

        AtomicReference<User> atomicReference = new AtomicReference<>();
        atomicReference.set(u1);

        // 成功
        System.out.println(atomicReference.compareAndSet(u1, u2) + "\t" + atomicReference.get());

        // 失败,因为已经变为u2
        System.out.println(atomicReference.compareAndSet(u1, u2) + "\t" + atomicReference.get());

    }
}

结果:

true    User(username=我是u2, age=15)
false    User(username=我是u2, age=15)

时间戳的原子引用

新增机制,修改版本号,类似于乐观锁

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);
            atomicReference.compareAndSet(101, 100);
        }, "Thread 1").start();

        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atomicReference.compareAndSet(100, 2020) + "\t" + atomicReference.get());
        }, "Thread 2").start();

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("-----ABA问题的解决-----");

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t第1次的版本号" + atomicStampedReference.getStamp());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomicStampedReference.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());
        }, "Thread 3").start();

        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t第1次的版本号" + stamp);
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean result = atomicStampedReference.compareAndSet(100, 200, stamp, stamp + 1);
            System.out.println(Thread.currentThread().getName() + "\t修改是否成功:" + result + "\t当前最新实际版本号:" + atomicStampedReference.getStamp());
            System.out.println(Thread.currentThread().getName() + "\t当前最新实际值:" + atomicStampedReference.getReference());
        }, "Thread 4").start();
    }
}

结果为:

-----模拟ABA问题的产生-----
true    2020
-----ABA问题的解决-----
Thread 3    第1次的版本号1
Thread 4    第1次的版本号1
Thread 3    第2次版本号2
Thread 3    第3次版本号3
Thread 4    修改是否成功:false    当前最新实际版本号:3
Thread 4    当前最新实际值:100

推荐阅读



Last modification:August 18, 2020
如果觉得我的文章对你有用,请随意赞赏