• 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面试_大厂高频面试题_阳哥 整理

  • 相关阅读:
    根据访问属性进行差异化数据加载
    前人挖坑,后人填坑
    也让盲人拥抱互联网
    谈谈D2
    Android数据库大批量数据插入优化
    framework中编译anroid工程并在模拟器上运行
    简单JNI使用demo
    解决javah生成c头文件时找不到android类库的问题
    JNI的native代码中打印日志到eclipse的logcat中
    Android.mk简介<转>
  • 原文地址:https://www.cnblogs.com/chaozhengtx/p/14435810.html
Copyright © 2020-2023  润新知