JVM之CAS(Compare and swap缩写)

目录

1. 由CAS展开的知识点

2. CAS简介

2.1 概念

2.2 操作过程

2.3 DEMO案例

3 CAS底层原理

3.1 Unsafe类(源码地址 jvm rt.jar\sun\misc\Unsafe)

3.2 Unsafe定义

3.3 CAS为什么保证原子性?

3.4 Unsafe类getAndAddInt 底层源码解析

3.4.1源码:

3.4.2 解析:

3.4.3 源码套入案例解析:

3.5 底层汇编

3.6 小结(为了加深印象)

4 CAS缺点

5 ABA问题

5.1 什么是ABA问题

5.2 如何解决ABA问题

5.2.1 原子引用(AtomicReference)说明

5.2.2 DEMO AtomicReference的使用

5.2.3 存在ABA问题的DEMO

5.2.4 原子引用版本号(类似时间戳)AtomicStampedReference

5.2.5 利用AtomicStampedReference解决ABA问题的DEMO


1. 由CAS展开的知识点

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

2. CAS简介

CAS(compareAndSet,compareAndExchange,compareAndSwap), 比较并交换

2.1 概念

CAS 是 compareAndSwap 的缩写,它是一条CPU并发原语。

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

2.2 操作过程

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器(在 Intel 处理器中,比较并交换通过指令的 cmpxchg 系列实现)会自动将该位置值更新为新值。否则,处理器不做任何操作。这个过程是原子的

再说的直白点就是先进行比较,如果相比较的两个值是相等的,那么就进行更新操作

2.3 DEMO案例

/**
 * CAS:compareAndSet,比较并交换
 * AtomicInteger
 */
public class CASDemo {

    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(5);
        System.out.println(atomicInteger.compareAndSet(5, 2019)+"\t current data: "+atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(5, 1024)+"\t current data: "+atomicInteger.get());
    }
}

//==============================下面是源码==============================
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
*
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates that
* the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

输出:

true     current data: 2019
false     current data: 2019

第一次打印,compareAndSet(期望值,要更新的值), 主内存值和期望值都是5,比较成功,内存值更新为2019

第二次打印,主内存值2019,期望值5,比较失败,无法更新值

3 CAS底层原理

自旋锁,Unsafe类

3.1 Unsafe类(源码地址 jvm rt.jar\sun\misc\Unsafe)

AtomicInteger i++ 源码:

JVM之CAS(Compare and swap缩写)_第1张图片

参数定义:

this是当前对象, valueOffset是对象内存地址偏移量,1是增加数值

AtomicInteger类 源码

JVM之CAS(Compare and swap缩写)_第2张图片

注意源码中value是被volatile修饰的,保证了多线程之间的内存可见性(即该值变更其他线程都知道)

3.2 Unsafe定义

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

CAS底层就是根据valueOffset内存中的偏移地址(相当于坐标),修改坐标所在的value值。 因为Unsafe就是根据内存偏移地址获取数据的。

注意Unsafe类中的所有方法都是native修饰的,也就是Unsafe类中的方法都直接调用操作系统底层资源执行相应任务。可以保证线程安全

3.3 CAS为什么保证原子性?

因为CAS汇编指令发出CPU原语,这是依赖硬件的功能,原语执行具有原子性,一定是连续执行,不会打断的

3.4 Unsafe类getAndAddInt 底层源码解析

3.4.1源码:

JVM之CAS(Compare and swap缩写)_第3张图片

JVM之CAS(Compare and swap缩写)_第4张图片

3.4.2 解析:

do中的操作:get,获取内存值

while中的操作:比较成功则交换值,跳出循环;失败则重新do操作

var1 是AtomicInteger对象本身

var2 该对象值的内存偏移量(即对象的引用地址)

var4 需要变动的数量, i++时,值为1,代表每次加1

var5 是用过var1 var2找出的主内存的真实值

用该对象当前的值与var5比较(即va1对象在var2地址的值是否是var5)

相同,则更新var5+var4并且返回4

不同,继续取值再比较,直至更新完成

do while这里就相当于自旋锁,属于乐观锁的思想

3.4.3 源码套入案例解析:

JVM之CAS(Compare and swap缩写)_第5张图片

假设线程A和线程B两个线程同时执行了getAndAddInt(var1:当前对象, var2:地址偏移量, var4:1)操作(分别跑在不同的CPU上)

1. AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3

2. 线程A和线程B拷贝主内存value值3的副本到各自的工作内存。

3. 线程A通过getIntVolatile(var1, var2)拿到主内存value值3,这时线程A被挂起。

4. 线程B通过getIntVolatile(var1, var2)获取到value值3并赋值给var5,此时刚好线程B没有被挂起并执行compareAndSwapInt方法,比较var5(期望值)和主内存值(真实值)是否相等,当前值均为3,相等。修改内存值为4,线程B结束

5. 这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值已经被其他线程抢先一步修改过,故线程A本次修改失败,只能重新获取内存值,再次比较。

6. 线程A重新获取value值,因为变量value被volatile修饰,所以线程A可以看到其它线程对它的修改,线程A继续执行compareAndSwapInt进行比较替换,直至成功。

3.5 底层汇编

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

JVM之CAS(Compare and swap缩写)_第6张图片

//Atomic 原子的

//cmp(compare比较)x(值)chg(并替换change)

3.6 小结(为了加深印象)

CAS 比较当前工作内存中的值和主内存中的值,相同执行规定操作,否则继续比较直到工作内存和主内存数值一致为止。

CAS 三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)

预期值A和内存位置V的值相同时,内存位置V的值修改为新值B,否则不处理。

4 CAS缺点

synchonized 一致性保证(加锁,同一时间段只有一个线程进行操作),并发量下降.

synchronized是悲观锁这种线程一旦得到锁,其他需要锁的线程就挂起的情况就是悲观锁

CAS操作的就是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。

CAS 不加锁,保证了一致性,但是需要多次比较,故:

  • 循环时间长,开销大(CAS比较失败会循环再次比较,长时间不成功对CPU开销很大)
  • 只能保证一个共享变量的原子性操作(对于多个共享变量操作时,循环CAS无法保证操作原子性,可用锁来保证)
  • 存在ABA问题

5 ABA问题

5.1 什么是ABA问题

一句话表示:狸猫换太子

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

举例:

线程A从内存位置V中取出X值, 线程B也从内存位置V中取出X值,且线程B进行了操作,将X值改为Y,然后又再次操作将Y数据改为X, 这是线程A进行CAS操作,发现内存中仍然是X,线程A操作成功。

ABA问题指的是多个线程同时执行,那么开始时其获得的值都是A,当一个线程修改了A为B,后续修改了B为A,那么其他线程修改时判断A仍然是A,认为其没有修改过,因此会CAS成功(即头尾一致,中间存在过变化)

ABA问题产生的影响取决于你的业务是否会因此受到影响.如果有影响那么解决思路一般是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。 在JDK1.5之后提供了AtomicStampedReference类来解决ABA问题,解决思路是保存元素的引用,引用相当于版本号,是每一个变量的标识,因此在CAS前判断下是否是同一个引用即可。

5.2 如何解决ABA问题

首先提出一个概念叫原子引用,之前的AtomicInteger是操作Integer数据,其他类型需要保证原子性时,可用AtomicReference、AtomicStampedReference(可解决ABA问题)。

5.2.1 原子引用(AtomicReference)说明

JVM之CAS(Compare and swap缩写)_第7张图片

5.2.2 DEMO AtomicReference的使用

//========================== 用户类 ==========================
package com.demo.jvm1_cas;

public class User {
    String userName;
    int 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 +
                '}';
    }

}

//========================== 分割线 main ==========================

package com.demo.jvm1_cas;

import java.util.concurrent.atomic.AtomicReference;

/**
 * AtomicReference 原子引用
 */
public class AtomicReferenceDemo {

    public static void main(String[] args) {

        User zhangsan = new User();
        zhangsan.setUserName("zhangsan");
        zhangsan.setAge(22);

        User lisi = new User();
        lisi.setUserName("lisi");
        lisi.setAge(25);

        AtomicReference atomicReference = new AtomicReference<>();
        atomicReference.set(zhangsan);

        System.out.println(atomicReference.compareAndSet(zhangsan,lisi) + "\t"+atomicReference.get().toString());
        System.out.println(atomicReference.compareAndSet(zhangsan,lisi) + "\t"+atomicReference.get().toString());
    }
}

输出

true    User{userName='lisi', age=25}
false    User{userName='lisi', age=25}

5.2.3 存在ABA问题的DEMO

package com.demo.jvm1_cas;

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

/**
 * ABA问题
 */
public class ABADemo {

    static AtomicReference atomicReference = new AtomicReference<>(100);

    public static void main(String[] args) {
        //原始值
        System.out.println(Thread.currentThread().getName() + "\t value = " + atomicReference.get().toString());

        new Thread(()-> {
            //第一次CAS
            atomicReference.compareAndSet(100,101);
            System.out.println(Thread.currentThread().getName() + "\t T1 CAS1 value = " + atomicReference.get().toString());

            //第二次CAS,ABA问题产生
            atomicReference.compareAndSet(101,100);
            System.out.println(Thread.currentThread().getName() + "\t T1 CAS2 value = " + atomicReference.get().toString());
        },"T1").start();

        new Thread(()-> {
            //为了保证T1线程完成了ABA操作,暂停T2线程1秒
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomicReference.compareAndSet(100,1024);
            System.out.println(Thread.currentThread().getName() + "\t T2 CAS3 value = " + atomicReference.get().toString());
        },"T2").start();
    }
}

输出
main     value = 100
T1     T1 CAS1 value = 101
T1     T1 CAS2 value = 100
T2     T2 CAS3 value = 1024

由此可以看出,T1线程中途修改value为101,后续又修改回100,引发了ABA问题,T2线程在T1线程结束后,T2线程操作CAS成功,将值改为1024,并没有发现T1线程曾经修改过原值。

5.2.4 原子引用版本号(类似时间戳)AtomicStampedReference

每操作一次,记录一次版本号

JVM之CAS(Compare and swap缩写)_第8张图片

5.2.5 利用AtomicStampedReference解决ABA问题的DEMO

package com.demo.jvm1_cas;

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

/**
 * 解决ABA问题
 */
public class AtomicStampReferenceDemo {

    static AtomicStampedReference atomicStampedReference = new AtomicStampedReference<>(100, 1);

    public static void main(String[] args) {
        new Thread(()-> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t 第1次版本号:" + stamp);
            //T1暂停一秒,保证T2线程也获取到第一次的版本号
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }

            //第一次CAS
            atomicStampedReference.compareAndSet(100,101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName() + "\t 第2次版本号:" + atomicStampedReference.getStamp());

            //ABA问题产生
            atomicStampedReference.compareAndSet(101,100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName() + "\t 第3次版本号:" + atomicStampedReference.getStamp());

        },"T1").start();

        new Thread(()-> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t 第1次版本号:" + stamp);

            //为了保证T1线程完成了ABA操作,暂停T2线程1秒
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            boolean result = atomicStampedReference.compareAndSet(100,2019, atomicStampedReference.getStamp(), stamp+1);
            System.out.println(Thread.currentThread().getName() + "\t 修改结果:" + result + "\t 当前最新版本号" + atomicStampedReference.getStamp());
            System.out.println(Thread.currentThread().getName() + "\t 当前最新值:" + atomicStampedReference.getReference().toString());
        },"T2").start();
    }
}

输出

T1     第1次版本号:1
T2     第1次版本号:1
T1     第2次版本号:2
T1     第3次版本号:3
T2     修改结果:false     当前版本号3
T2     当前最新值:100

由此可以看出,当T1发生了ABA问题后,T2依据最初获取的版本号进行CAS操作失败,解决多线程无感知发生ABA的问题

6 应用场景

CAS操作在Java中的应用很广泛,比如ConcurrentHashMap,ReentrantLock等,其常被用来解决独占锁对线程阻塞而导致的性能低下问题,是高效并发必备的一种优化方法.

6.1 CAS在多线程问题中起到了什么作用?

多线程问题归根结底要解决的是可见性,有序性,原子性三大问题,大家都知道JVM提供的volatile可以保证可见性与有序性,但是无法保证原子性,换句话说 volatile + CAS实现原子性操作 = 线程安全 = 高效并发,那么CAS就是用来实现这个操作的原子性.

7 参考文献

【1】Java面试_高频重点面试题 (第一、二、三季)_ 面试 第1、2、3季_柴林燕_周阳_哔哩哔哩_bilibili

【2】Java--CAS操作分析 - 云+社区 - 腾讯云 (tencent.com)

你可能感兴趣的:(JVM,jvm,java)