Java并发编程 之CAS

面试回答CAS

CAS — UnSafe — CAS底层思想 — ABA — 原子引用更新 — 如何规避ABA问题。

面试回答: 设置值之前,先读取内存中的当前值,在尝试设置值得时候,比较下内存中的值是否有变动,如果未变动,设置值成功;如果有变动,重新读取内存中的值,再次尝试设置值,这样一直重复运行。
一次设置值过程就是一次CAS操作,CAS在底层的硬件级别就保证了原子性,同一时间只有一个线程可以执行CAS,先比较再设置,其他的线程的CAS同时间去执行此时会失败。
通过Unsafe类实现的CAS,Unsafe里调用的是native方法。

Unsafe类+CAS思想,比较并交换直到比较成功,即:自旋。
比较并交换,判断内存的值是否为预期值,是则更新,否放弃,过程是原子性的。
是用UnSafe类实现的,它是JDK提供的,所有的方法都用了native修饰,直接操作底层资源了。
CAS:比较并交换

AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5, 2019));
System.out.println(atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5, 2019));
System.out.println(atomicInteger.get());

结果:

true
2019
false
2019

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

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

原子整型类AtomicInteger在多线程下进行i++操作,不用Synchronized也能保证线程安全,就是因为底层使用了UnSafe类。

变量ValueOffset,表示该变量在内存中的偏移地址,因为UnSafe就是根据内存偏移地址获取数据的。
在这里插入图片描述
在这里插入图片描述

变量value用volatile修饰,保证了多线程之间的可见性。

AtomicInteger 原理图

一个线程进行累加:
在这里插入图片描述
两个线程同时进行累加:
在这里插入图片描述

CAS是什么

CAS的全称为Compare-And-Swap,它是一条CPU并发原语。
它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。

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

unsafe.getAndAddInt(this, valueOffset, 1) 方法解读

public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
    
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
        //        compareAndSwapInt(Object o, long offset, int expected, int x)
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    return var5;
}

var1: 传入的对象,这里是AtomicInteger对象本身
var2: 传入对象在内存中的偏移量
var4: 需要变动的数值
var5: 期望值,通过 var1和 var2 得到内存中真实的值
compareAndSwapInt读取传入对象o在内存中偏移量为offset位置的值与期望值expected(var5)作比较。
如果相同,更新var5+var4并且返回true。
如果不同,继续取值然后比较,直到更新完成。

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

Unsafe.compareAndSwapInt() 方法解读

public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);

此方法是Java的native方法,并不由Java语言实现。
方法的作用是,读取传入对象o在内存中偏移量为offset位置的值与期望值expected作比较。
相等就把x值赋值给offset位置的值。方法返回true。
不相等,就取消赋值,方法返回false。
这也是CAS的思想,及比较并交换。用于保证并发时的无锁并发的安全性。

为什么要用CAS而不用Synchronized

Synchronized加锁同一时间段只能有一个线程访问,并发性能差。
CAS,没有加锁,反复比较,直到比较成功为止,既保证了一致性,又提高了并发性。

CAS缺点

  • 循环时间长开销很大
    可以看到 getAndAddInt 方法执行时,有个 do while
    如果CAS失败,会一直进才尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。
  • 只能保证一个共享变量的原子性
    对多个共享变量操作时,循环CAS无法保证操作的原子性,这个时候就可以用锁来保证原子性。
  • 会导致ABA问题

ABA问题

狸猫换太子。
CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差之内会导致数据的变化。
比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中任然是A,然后线程one操作成功。

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

解决ABA问题

原子引用:AtomicReference
带时间戳的原子引用:AtomicStampedReference 可以解决ABA问题,设置值得时候可以带版本。

public class ABADemo {

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

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

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

        System.out.println("ABA问题的产生");

        new Thread(() -> {
            System.out.println(atomicReference.compareAndSet(100, 101));
            System.out.println(atomicReference.compareAndSet(101, 100));
        }, "t1").start();


        new Thread(() -> {
            try {
                // 暂停一秒保证线程1完成一次ABA
                TimeUnit.SECONDS.sleep(1);
                System.out.println(atomicReference.compareAndSet(100, 2020) + "\t" + atomicReference.get());
            } catch (InterruptedException e) {
            }
        }, "t2").start();

        TimeUnit.SECONDS.sleep(2);

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

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t 第1次版本号" + stampedReference.getStamp() + "\t值是" + stampedReference.getReference());
            try {
                // 暂停一秒t3线程
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
            }
            stampedReference.compareAndSet(100, 101, stampedReference.getStamp(), stampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "\t 第2次版本号" + stampedReference.getStamp() + "\t值是" + stampedReference.getReference());
            stampedReference.compareAndSet(101, 100, stampedReference.getStamp(), stampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "\t 第3次版本号" + stampedReference.getStamp() + "\t值是" + stampedReference.getReference());
        }, "t3").start();

        new Thread(() -> {
            int stamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t 第1次版本号" + stamp + "\t值是" + stampedReference.getReference());
            try {
                // 保证线程3完成一次ABA
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
            }
            boolean result = stampedReference.compareAndSet(100, 2020, stamp, stamp + 1);
            System.out.println(Thread.currentThread().getName() + "\t 是否修改成功" + result);
            System.out.println(Thread.currentThread().getName() + "\t 最新版本号" + stampedReference.getStamp() + "\t最新的值" + stampedReference.getReference());
        }, "t4").start();
        
    }
}

结果:

ABA问题的产生
true
true
true	2020
ABA问题的解决
t3	 第1次版本号1	值是100
t4	 第1次版本号1	值是100
t3	 第2次版本号2	值是101
t3	 第3次版本号3	值是100
t4	 是否修改成功false
t4	 最新版本号3	最新的值100
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值