CAS和ABA问题

1 CAS

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

实例

底层原理

  1. 自旋锁
  2. UnSafe(来自于:rt.jat/sun/misc/Unsafe.class):操作系统底层方法的类,原子性由CPU原语保证,getAndIncrement()方法的底层源码:

能够看到,atomicInteger.getAndIncrement() 方法调用了 unsafe 类的 getAndAddInt() 方法;Unsafe 是 CAS 的核心类,由于 Java 方法无法直接访问底层系统,需要通过本地(Native)方法来访问,Unsafe 相当于一个后门,基于该类可以直接操作特定的内存数据。Unsafe 类存在sun.misc 包中,其内部方法操作可以像 C 的指针一样直接操作内存,因为 Java 中的 CAS 操作的执行依赖于 Unsafe 类的方法。
其中变量 valueOffset 表示该变量值在内存中的偏移地址(即内存地址),因为 Unsafe 就是根据内存偏移地址获取数据的。

简单小结:

  1. 比较当前工作内存中的值和主内存中的值,如果相同执行规定操作,否则继续比较直到主内存和工作内存的值一致为止。
  2. CAS 有3个操作数,内存值V,旧的预期值A,要修改更新值B,当且仅当预期值A和内存值V相同时,将内存值修改为B,否则什么都不做。

CAS 缺点

  • 循环时间长,开销大
  • 只能保证一个共享变量的操作
  • 引出ABA问题

2 ABA 问题

从 AtomicInteger 引出下面的问题:
CAS -> Unsafe -> CAS 底层思想 -> ABA -> 原子引用更新 -> 如何规避 ABA 问题

可以理解为 狸猫换太子。就是t1和t2两个线程同时操作主内存中的A时,t1、t2分别将A拷贝到自己的工作内存进行操作,其中t2线程完成较快,它将A改成B,后又将B改回A;当t1线程写回时,发现预期值是A,所以将操作后的结果写回。最后结果看似正常,其实过程中存在着很大的问题。

原子引用

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

解决 ABA 问题

原子引用 + 版本号(时间戳):根据版本号判断当前数据是否经过修改。

public class ABADemo {

    /**
     * 普通的原子引用包装类
     */
    static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);

    // 传递两个值,一个是初始值,一个是初始版本号
    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);

    public static void main(String[] args) {

        System.out.println("============以下是ABA问题的产生==========");

        new Thread(() -> {
            // 把100 改成 101 然后在改成100,也就是ABA
            atomicReference.compareAndSet(100, 101);
            atomicReference.compareAndSet(101, 100);
        }, "t1").start();

        new Thread(() -> {
            try {
                // 睡眠一秒,保证t1线程,完成了ABA操作
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 把100 改成 101 然后在改成100,也就是ABA
            System.out.println(atomicReference.compareAndSet(100, 2019) + "	" + atomicReference.get());

        }, "t2").start();

        System.out.println("============以下是ABA问题的解决==========");

        new Thread(() -> {

            // 获取版本号
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "	 第一次版本号" + stamp);

            // 暂停t3一秒钟
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 传入4个值,期望值,更新值,期望版本号,更新版本号
            atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);

            System.out.println(Thread.currentThread().getName() + "	 第二次版本号" + atomicStampedReference.getStamp());

            atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);

            System.out.println(Thread.currentThread().getName() + "	 第三次版本号" + atomicStampedReference.getStamp());

        }, "t3").start();

        new Thread(() -> {

            // 获取版本号
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "	 第一次版本号" + stamp);

            // 暂停t4 3秒钟,保证t3线程也进行一次ABA问题
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp+1);

            System.out.println(Thread.currentThread().getName() + "	 修改成功否:" + result + "	 当前最新实际版本号:" + atomicStampedReference.getStamp());

            System.out.println(Thread.currentThread().getName() + "	 当前实际最新值" + atomicStampedReference.getReference());


        }, "t4").start();

    }
}

我们能够发现,线程 t3,在进行 ABA 操作后,版本号变更成了 3,而线程 t4 在进行操作的时候,就出现操作失败了,因为版本号和当初拿到的不一样 。


根据 Java面试_大厂高频面试题_阳哥 整理

原文地址:https://www.cnblogs.com/chaozhengtx/p/14435810.html