高并发学习笔记—— CAS 和ABA问题

Posted Johnny*

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了高并发学习笔记—— CAS 和ABA问题相关的知识,希望对你有一定的参考价值。

在这里插入图片描述

CAS

什么是CAS

CAS是Compare-And-Swap,即比较并交换。
基本思想是:拿工作内存中的副本,即预期值(expect)和主内存中某个位置的值进行比较,如果相同则主内存值没有被修改过,那么将内存的值更新为新的值(update)。否则继续比较直至主内存和工作内存的值一致为止。

CAS是原子性的。这是因为JVM的CAS操作是利用了处理器提供的CMPXCHG指令实现的。由于CAS是一种系统原语,而原语的执行必须是连续的,在执行的过程中不会被中断,因此不会造成所谓的数据不一致问题,也就是说CAS是线程安全的。

CAS设计unsafe类、自旋锁。

具体实现

CAS并发原语具体体现是通过sun.misc.Unsafe类中的各个方法(这些方法都是native的)。调用UnSafe类的CAS方法,JVM会帮我们实现CAS汇编指令,这是一种完全依赖硬件的功能,通过它实现了原子操作。

示例


public class CASDemo {
    public static void main(String[] args) throws InterruptedException {

        AtomicInteger atomicInteger = new AtomicInteger(5);

        new Thread(()->{
            System.out.println(Thread.currentThread().getName());
            System.out.println(atomicInteger.compareAndSet(5, 2019)+"\\t" +"当前值:"+atomicInteger.get()  );
             },"T1").start();

        TimeUnit.SECONDS.sleep(2);
        new Thread(()->{
            System.out.println(Thread.currentThread().getName());
            System.out.println(atomicInteger.compareAndSet(5, 2021)+"\\t" +"当前值:"+atomicInteger.get()  );
            },"T2").start();


    }
}

运行结果:

T1
true	当前值:2019
T2
false	当前值:2019

解释:

有两个线程T1和T2,根据JMM模型,两个线程都有对值为5的atomicInteger 变量副本的拷贝存放在格子的工作内存中。T1线程向将修改主内存的值为2019,在写入主内存时调用compareAndSet方法进行比较并交换,发现期望值(5)和主内存中的值(5)一致,即主内存的值没有被修改过。所以将主内存中的值更新为2019同时由于volatile对线程的可见性,会通知其他线程。
而T2线程在进行比较并交换时发现变量副本拷贝值(5)与主内存中的值(2019)不一致,说明主内存是被修改过的了。所以更新失败。
在这里插入图片描述

CAS 底层原理

通过AtomicInteger类的getAndIncrement()方法了解CAS的底层原理

1、 内存偏移量VALUE

AtomicInteger类的getAndIncrement()方法
在这里插入图片描述
其中U是UnSafe类对象,VALUE为通过调用的Unsafe类的 objectFieldOffset(Class<?> c, String name)方法计算出的内存地址偏移量
在这里插入图片描述
2、 Unsafe类

Unsafe是CAS实现的核心类,由于Java无法直接访问内存,需要通过调用本地方法(native修饰的方法,底层调用的是C语言的库函数)来访问。Unsafe相当于Java的一个后门,基于该类可以直接操作特定内存的数据。
这是因为Unsafe的所有方法都是native方法,其中不少方法中必须提供原始地址(内存地址)和被替换对象的地址,并且偏移量可以通过Unsafe类中的方法获得。也就是说Unsafe类是直接操作系统底层资源执行相应任务。这使得Java拥有像C语言指针一样操作内存空间的能力。

Unsafe虽然在一定程度上提高 了执行效率但是 也带来 了指针的不安全,这是因为:
不受JVM管理。Unsafe操作内存无法被JVM GC,需要我们手动GC,稍有不慎就会出现内存泄漏。

3、自旋锁

在这里插入图片描述v: 就是我们从主内存中拷贝到工作内存中的值,每次都要从主内存中拿到最新的值到本地的工作内存。然后调用weakCompareAndSetInt(),该方法本质调用compareAndSet方法进行期望值(v)和主内 存中的值(通过o和offset可以获取)的比较。

  1. 如果相同则更新主内存值为 v +delta并返回true
  2. 如果不相同则返回false,while循环不退出,会继续取值。

4、 weakCompareAndSetInt方法

	//Unsafe类
    @HotSpotIntrinsicCandidate
    public final boolean weakCompareAndSetInt(Object o, long offset,
                                              int expected,
                                              int x) {
        return compareAndSetInt(o, offset, expected, x);
    }
    //Unsafe类
    @HotSpotIntrinsicCandidate
    public final native boolean compareAndSetInt(Object o, long offset,
                                                 int expected,
                                                 int x);

底层汇编

Unsafe类中的compareAndSwapInt是一个本地方法,该方法的实现位于unsafe.cpp中

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

缺点

  1. 循环时间长,开销大。do{} while(); 如果CAS长时间不成功,即每次内存值都被其他线程修改,那么该线程有可能会 一直自旋下去,这会会给CPU带来非常大的执行开销。
  2. 只能保证一个共享变量的原子操作。顺带提一下,从Java1.5开始JDK提供了原子 引用AtomicReference类来保证引用对象之间的原子性,就可以将多个变量放在一个对象中来进行CAS操作。
  3. 存在ABA问题。

ABA问题

在这里插入图片描述
假设有T1、T2两个线程分别需要10s和2s,一开始主内存值为A,然后T2在执行过程中将主内存值修改为B,最后又修改回A。中间发生了猫腻。T1线程线程在第10s时发现主内存值仍为A,就认为未被其他线程修改过,实际上中间过程经历了狸猫换太子。这就是ABA问题。

概括来说,ABA问题就是在进行获取 主内存值的时候,该内存值在我们写入主内存前已经被修改了N次,但是最后修改回原来的值。

CAS 会导致ABA问题

CAS算法实现的一个重要前提是需要取出内存 中某个时刻的数据并在当下时刻比较并替换,那么从取出到替换这个时间段可能会有其他线程操纵导致数据的变化。

原子引用

原子引用其实和原子包装类概念类似,就是将一个Java类用原子引用类包装起来,那么这个类就具备了原子性。原子引用使得CAS可以操作多个共享变量。

package CASDemo;

import java.util.concurrent.atomic.AtomicReference;

/**
 * @author Johnny Lin
 * @date 2021/6/12 19:55
 */

class User{
    private String userName;
    private 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 zs = new User("zs", 23);
        User ls = new User("ls", 28);

        AtomicReference<User> atomicReference = new AtomicReference<>(zs);
        System.out.println(atomicReference.compareAndSet(zs, ls) +"\\t"+atomicReference.get());

        System.out.println(atomicReference.compareAndSet(zs, new User("ww", 35)) +"\\t"+atomicReference.get());

    }

}

true	User{userName='ls', age=28}
false	User{userName='ls', age=28}

ABA问题的解决

AtomicStampedReference引入一个类似版本号的概念stamp,如果版本号(每修改值一次版本号就会自增1)没有匹配上,即使值个预期是相同的,也不能修改。这样就解决了ABA问题。
这与SVN或git的版本冲突时类似的,如果远程仓库被其他人修改了,必须先同步本地仓库,将远程仓库pull到本地,同步号版本号之后才能push本地仓库。

package CASDemo;

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

/**
 * @author Johnny Lin
 * @date 2021/6/12 20:08
 */
public class AtomicStpRefDemo {

    static AtomicReference<String> ar = new AtomicReference<>("A");
    static  AtomicStampedReference<String> asr = new AtomicStampedReference<>("A", 1);

    public static void main(String[] args) {


        System.out.println("==========以下是ABA问题的产生===========");
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"\\t come in###");
            //T1线程休眠 保证T2线程完成一次ABA操作
            try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(ar.compareAndSet("A", "C")+"\\t"+ar.get());
        }, "T1").start();

        //T2线程执行一次ABA 操作
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"\\t come in");
            //T2线程先将主内存中的值从A修改到B 在从B 修改回A
            ar.compareAndSet("A", "B");
            ar.compareAndSet("B", "A");
        }, "T2").start();


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

        System.out.println("==========以下是ABA问题的解决===========");
        //public AtomicStampedReference(V initialRef, int initialStamp)


        //T3线程执行一次ABA操作
        new Thread(() -> {
            int stamp = asr.getStamp(); //获取版本号
            System.out.println(Thread.currentThread().getName() + "\\t 第一次版本号" + stamp);

            //T3线程休眠1秒
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }

            //进行ABA操作
            asr.compareAndSet("A", "B", stamp, asr.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "\\t 第二次版本号" + asr.getStamp()+" 修改后的值\\t"+ asr.getReference());

            asr.compareAndSet("B", "A", asr.getStamp(), asr.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "\\t 第三次版本号" + asr.getStamp()+" 修改后的值\\t"+ asr.getReference());

        }, "T3").start();



        new Thread(() -> {
            //T4睡眠4秒保证T3执行完一次ABA了
            try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); }
            boolean b = asr.compareAndSet("A", "C", 1, asr.getStamp() + 1);
            System.out.println("是否修改成功:" + b +"\\t当前值为"+ asr.getReference());

        }, "T4").start();


    }

}

执行结果
在这里插入图片描述

来源

B尚硅谷Java大厂面试题第二季(java面试必学,周阳主讲)

以上是关于高并发学习笔记—— CAS 和ABA问题的主要内容,如果未能解决你的问题,请参考以下文章

高并发面试必问:CAS 引起ABA问题解决方案

CAS与ABA问题产生和解决

CAS,在硬件层面为并发安全保驾护航

CAS,在硬件层面为并发安全保驾护航

CAS 和 ABA 问题

Java并发编程原理与实战四十三:CAS ---- ABA问题