02_CAS底层原理

概念

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

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

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

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

        /**
         * 一个是期望值,一个是更新值,但期望值和原来的值相同时,才能够更改
         * 假设三秒前,我拿的是5,也就是expect为5,然后我需要更新成 8
         */
        System.out.println(atomicInteger.compareAndSet(5,8));
        System.out.println(atomicInteger.compareAndSet(5,9));
    }
}

在这里插入图片描述
这是因为我们执行第一个的时候,期望值和原本值是满足的,因此修改成功,但是第二次后,主内存的值已经修改成了8,不满足期望值,因此返回了false,本次写入失败
在这里插入图片描述
这个就类似于SVN或者Git的版本号,如果没有人更改过,就能够正常提交,否者需要先将代码pull下来,合并代码后,然后提交

底层原理

首先我们先看看 atomicInteger.getAndIncrement()方法的源码
在这里插入图片描述
从这里能够看到,底层又调用了一个unsafe类的getAndAddInt方法
在这里插入图片描述
Unsafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(Native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定的内存数据。Unsafe类存在sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中的CAS操作的执行依赖于Unsafe类的方法。

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

为什么Atomic修饰的包装类,能够保证原子性,依靠的就是底层的unsafe类

变量valueOffset
表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
从这里我们能够看到,通过valueOffset,直接通过内存地址,获取到值,然后进行加1的操作

valueOffset 这里可以直接理解成内存地址,更直观点可以理解我们座位:比如:第三排第5号

变量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不加锁,保证一致性,但是需要多次比较

  • 循环时间长,开销大(因为执行的是do while,如果比较不成功一直在循环,最差的情况,就是某个线程一直取到的值和预期值都不一样,这样就会无限循环)
  • 只能保证一个共享变量的原子操作
    - 当对一个共享变量执行操作时,我们可以通过循环CAS的方式来保证原子操作
    - 但是对于多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候只能用锁来保证原子性
  • 引出来ABA问题?

ABA问题

我们上面说了cas会引发ABA问题,但是什么是ABA问题

CAS -> Unsafe -> CAS底层思想 -> ABA -> 原子引用更新 -> 如何规避ABA问题

什么是ABA问题

在这里插入图片描述
假设现在有两个线程,分别是T1 和 T2,然后T1执行某个操作的时间为10秒,T2执行某个时间的操作是2秒,最开始AB两个线程,分别从主内存中获取A值,但是因为B的执行速度更快,他先把A的值改成B,然后在修改成A,然后执行完毕,T1线程在10秒后,执行完毕,判断内存中的值为A,并且和自己预期的值一样,它就认为没有人更改了主内存中的值,就快乐的修改成B,但是实际上 可能中间经历了 ABCDEFA 这个变换,也就是中间的值经历了狸猫换太子。

所以ABA问题就是,在进行获取主内存值的时候,该内存值在我们写入主内存的时候,已经被修改了N次,但是最终又改成原来的值了

简言之: A->B->A

CAS导致ABA问题

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

比如说一个线程one从内存位置V中取出A,这时候另外一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功

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

ABA问题

CAS只管开头和结尾,也就是头和尾是一样,那就修改成功,中间的这个过程,可能会被人修改过

原子引用

原子引用其实和原子包装类是差不多的概念,就是将一个java类,用原子引用类进行包装起来,那么这个类就具备了原子性

class User {
    String userName;
    int age;

    public User(String userName, int age) {
        this.userName = userName;
        this.age = age;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "userName='" + userName + '\'' +
                ", age=" + age +
                '}';
    }
}
public class AtomicReferenceDemo {

    public static void main(String[] args) {

        User z3 = new User("z3", 22);

        User l4 = new User("l4", 25);

        // 创建原子引用包装类
        AtomicReference<User> atomicReference = new AtomicReference<>();

        // 现在主物理内存的共享变量,为z3
        atomicReference.set(z3);

        // 比较并交换,如果现在主物理内存的值为z3,那么交换成l4
        System.out.println(atomicReference.compareAndSet(z3, l4) + "\t " + atomicReference.get().toString());

        // 比较并交换,现在主物理内存的值是l4了,但是预期为z3,因此交换失败
        System.out.println(atomicReference.compareAndSet(z3, l4) + "\t " + atomicReference.get().toString());
    }
}

代码演示ABA问题

public class ABADemo {


   static AtomicReference<Integer> referenceInteger = new AtomicReference<>(100);

    public static void main(String[] args) {
        //ABA问题的产生
        new Thread(()->{
            referenceInteger.compareAndSet(100,101);
            referenceInteger.compareAndSet(101,100);
        },"t1").start();

        new Thread(()->{
           try {
               //睡眠一秒确保线程t1进行完成
               TimeUnit.SECONDS.sleep(1);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
            System.out.println(referenceInteger.compareAndSet(100,2019));
        },"t2").start();

    }
}

我们发现,它能够成功的修改,这就是ABA问题
在这里插入图片描述
新增一种机制,也就是修改版本号,类似于时间戳的概念

T1: 100 1 2019 2

T2: 100 1 101 2 100 3

如果T1修改的时候,版本号为2,落后于现在的版本号3,所以要重新获取最新值,这里就提出了一个使用时间戳版本号,来解决ABA问题的思路

AtomicStampedReference

时间戳原子引用,来这里应用于版本号的更新,也就是每次更新的时候,需要比较期望值和当前值,以及期望版本号和当前版本号

public class ABADemo2 {


   static AtomicReference<Integer> referenceInteger = new AtomicReference<>(100);

   static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100,1);
    public static void main(String[] args) {
       new Thread(()->{
           System.out.println("线程t1当前版本号:"+stampedReference.getStamp());
           try {
               TimeUnit.SECONDS.sleep(1);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           stampedReference.compareAndSet(100,101,stampedReference.getStamp(),stampedReference.getStamp()+1);
           System.out.println("线程t1当前版本号:"+stampedReference.getStamp());
           stampedReference.compareAndSet(101,102,stampedReference.getStamp(),stampedReference.getStamp()+1);
           System.out.println("线程t1当前版本号:"+stampedReference.getStamp());
           stampedReference.compareAndSet(102,100,stampedReference.getStamp(),stampedReference.getStamp()+1);
           System.out.println("线程t1当前版本号:"+stampedReference.getStamp());
       },"t1").start();
       new Thread(()->{
           int stamp=stampedReference.getStamp();
           System.out.println("线程"+Thread.currentThread().getName()+"\t 版本号"+stamp);
            try {
              TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
              e.printStackTrace();
            }
           System.out.println(stampedReference.getReference());
           System.out.println(stampedReference.compareAndSet(100,2019,stamp,stamp+1));
       },"t2").start();

    }
}

在这里插入图片描述
我们能够发现,线程t1,在进行ABA操作后,版本号变更成了4,而线程t2在进行操作的时候,就出现操作失败了,因为版本号和当初拿到的不一样

CAS 原理差不多了,但是我们知道:

CAS底层实现是在一个死循环中不断地尝试修改目标值,直到修改成功。如果竞争不激烈的时候,修改成功率很高,否则失败率很高。在失败的时候,这些重复的原子性操作会耗费性能。(不停的自旋,进入一个无限重复的循环中)

这种问题难得就没有优化的方案吗?

LongAdder(CAS机制优化) 下一篇介绍LongAdder

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值