JUC并发编程(11)--- 深入理解CAS

Posted 小样5411

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JUC并发编程(11)--- 深入理解CAS相关的知识,希望对你有一定的参考价值。

一、初识CAS

什么是CAS?
CAS就是compareAndSet的缩写,意思是如果我期望(except)的值达到了就更新,否则就不更新,并且CAS是CPU的并发原语!执行效率非常高。
在这里插入图片描述

//CAS,compareAndSet:比较并交换
public class CASDemo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2020);
        System.out.println(atomicInteger.compareAndSet(2020, 2021));
        System.out.println(atomicInteger.get());
    }
}

运行后,compareAndSet返回的是boolean,成功则可以get(),获取更新后的元素
在这里插入图片描述

二、深入理解CAS

我们Ctrl点击compareAndSet,进入底层实现,发现是通过调用unsafe返回实现的,下面讲的compareAndSwapInt()和getAndIncrement()都是unsafe下的方法。
在这里插入图片描述
再点击unsafe
在这里插入图片描述
我们知道Java无法直接操作内存,但可以通过调用c++来操作内存,unsafe类就相当于java留的后门,可以用来操作内存

我们之前JUC文章说过getAndIncrement方法有别于普通自增方法,底层也是用unsafe实现的,那么我们现在来填坑,简单看看getAndIncrement()自增的效果

//CAS,compareAndSet:比较并交换
public class CASDemo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2020);
        System.out.println(atomicInteger.compareAndSet(2020, 2021));
        System.out.println(atomicInteger.get());

        atomicInteger.getAndIncrement();//自增
        System.out.println(atomicInteger.compareAndSet(2020, 2021));
        System.out.println(atomicInteger.get());
    }
}

在这里插入图片描述
我们ctrl点击getAndIncrement,再ctrl点击getAndAddInt
在这里插入图片描述

CAS底层是比较并交换
在这里插入图片描述
原理:CAS(compareAndSet)底层实际上是compareAndSwapInt,就是期望值和var5这个内存中的值比较,如果相同则vat5+var4,即var5+1。这里使用CPU原语,执行效率很高,并且do…while表示自旋锁,意思就是如果不是期望的值,就一直循环,直到是期望值才跳出。
在这里插入图片描述

但这样有个缺点,就是可能会出现ABA问题

什么是ABA问题?
举个例子,假如有线程A和线程B,线程A期望atomicInteger=1,而线程B执行先把atomicInteger变为2,后面又变为1。但是线程A获取的时候依然是1,根本不知道线程B把atomicInteger修改过,认为这个atomicInteger一直没有改变过。这就叫ABA问题

//CAS,compareAndSet:比较并交换
public class CASDemo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(2020);
        //捣乱的线程
        System.out.println(atomicInteger.compareAndSet(2020, 2021));
        System.out.println(atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(2021, 2020));
        System.out.println(atomicInteger.get());

        //期望的线程
        System.out.println(atomicInteger.compareAndSet(2020, 6666));
        System.out.println(atomicInteger.get());
    }
}

如代码,先由2020变为2021,再由2021变为2020,下面的都不知道被修改过,依然返回true,更新为6666,这样是不行的,我们需要让他就算中间修改过也能知道。
在这里插入图片描述

怎么解决呢?
原子引用(AtomicStampedReference)就可以解决这个问题,就是加上一个时间戳Stamp(可理解成版本号),只要被改动过,版本号就会+1,必须版本号和期望值都符合,那么才能更新。

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;

public class CASDemo1 {
    public static void main(String[] args) throws InterruptedException {
        AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1,1);
        System.out.println("************ABA问题的解决*************");

        //线程A
        new Thread(()->{
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\\t 第一次的版本号" +stamp);
            try {
                TimeUnit.SECONDS.sleep(1);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"\\t compareAndSet="+atomicStampedReference.compareAndSet(1, 2, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
            System.out.println(Thread.currentThread().getName()+"\\t 第二次的版本号" +atomicStampedReference.getStamp());
            System.out.println(Thread.currentThread().getName()+"\\t compareAndSet="+atomicStampedReference.compareAndSet(2, 1, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));
            System.out.println(Thread.currentThread().getName()+"\\t 第三次的版本号" +atomicStampedReference.getStamp());
        },"A").start();

        //线程B
        new Thread(()->{
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\\t 第一次的版本号" +stamp);
            try {
                TimeUnit.SECONDS.sleep(2);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            boolean result = atomicStampedReference.compareAndSet(1,4,stamp,stamp+1);
            System.out.println(Thread.currentThread().getName()+"\\t 修改是否成功" +result+"\\t A版本号:"+atomicStampedReference.getStamp()+"\\t B版本号"+(stamp+1));

            System.out.println(Thread.currentThread().getName()+"\\t 当前最新期望值" +atomicStampedReference.getReference());
        },"B").start();
    }
}


在这里插入图片描述

加一个版本号stamp,必须stamp和期望值都符合,那么才可更改,表示没有发生ABA问题,不然版本号不同,那中间一定被修改过。

以上是关于JUC并发编程(11)--- 深入理解CAS的主要内容,如果未能解决你的问题,请参考以下文章

并发编程之深入理解CAS

JUC并发编程07:单例模式CAS算法和原子引用

JUC并发编程线程池及相关面试题 详解

一文总结 JUC 并发编程

猿创征文 | 深入理解高并发编程 ~ 开篇

JUC并发编程 -- 并发/并行