• 原子类的ABA问题


    原子类AtomicInteger的ABA问题

    连环套路

    从AtomicInteger引出下面的问题

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

    ABA问题是什么

    狸猫换太子

    假设现在有两个线程,分别是T1 和 T2,然后T1执行某个操作的时间为10秒,T2执行某个时间的操作是2秒,最开始AB两个线程,分别从主内存中获取A值,但是因为B的执行速度更快,他先把A的值改成B,然后在修改成A,然后执行完毕,T1线程在10秒后,执行完毕,判断内存中的值为A,并且和自己预期的值一样,它就认为没有人更改了主内存中的值,就快乐的修改成B,但是实际上 可能中间经历了 ABCDEFA 这个变换,也就是中间的值经历了狸猫换太子。

    所以ABA问题就是,在进行获取主内存值的时候,该内存值在我们写入主内存的时候,已经被修改了N次,但是最终又改成原来的值了

    CAS导致ABA问题

    CAS算法实现了一个重要的前提,需要取出内存中某时刻的数据,并在当下时刻比较并替换,那么这个时间差会导致数据的变化。

    比如说一个线程one从内存位置V中取出A,这时候另外一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功

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

    ABA问题

    CAS只管开头和结尾,也就是头和尾是一样,那就修改成功,中间的这个过程,可能会被人修改过

    原子引用

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

    /**
     * 原子类引用
     */
    @Data
    @AllArgsConstructor
    class User {
        String userName;
        int age;
    }
    public class AtomicReferenceDemo {
    
        public static void main(String[] args) {
    
            User aaa = new User("aaa", 20);
            User bbb = new User("bbb", 30);
    
            // 创建原子引用包装类
            AtomicReference<User> atomicReference = new AtomicReference<>();
            // 现在主物理内存的共享变量,为aaa
            atomicReference.set(aaa);
    
            // 比较并交换,如果现在主物理内存的值为aaa,那么交换成bbb
            System.out.println(atomicReference.compareAndSet(aaa, bbb) + "	 " + atomicReference.get().toString());
            // 比较并交换,现在主物理内存的值是bbb了,但是预期为aaa,因此交换失败
            System.out.println(atomicReference.compareAndSet(aaa, bbb) + "	 " + atomicReference.get().toString());
        }
    }
    

    基于原子引用的ABA问题

    我们首先创建了两个线程,然后T1线程,执行一次ABA的操作,T2线程在一秒后修改主内存的值

    /**
     * 基于CAS引出ABA问题
     */
    public class ABADemo {
    
        static AtomicReference<Integer> atomicReference = new AtomicReference<Integer>(100);
    
        public static void main(String[] args) {
            new Thread(()->{
                // 把100 改成 127 然后在改成100,也就是ABA
                atomicReference.compareAndSet(100, 127);
                //特别强调在AtomicReference(Integer)中value超出-128~127,会生成一个新的对象而造成无法修改
                //但是在AtomicInteger中则不会存在这样的问题
                atomicReference.compareAndSet(127, 100);
            },"t1").start();
    
            new Thread(()->{
                try {
                    // 睡眠一秒,保证t1线程,完成了ABA操作
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 把100 改成 127 然后在改成100,也就是ABA
                System.out.println(atomicReference.compareAndSet(100, 2021)+"	"+atomicReference.get());
            },"t2").start();
        }
    }
    

    我们发现,它能够成功的修改,这就是ABA问题

    解决ABA问题

    新增一种机制,也就是修改版本号,类似于时间戳的概念

    T1: 100 1 2020 2

    T2: 100 1 127 2 100 3

    如果T1修改的时候,版本号为2,落后于现在的版本号3,所以要重新获取最新值,这里就提出了一个使用时间戳版本号,来解决ABA问题的思路

    AtomicStampedReference

    时间戳原子引用,来这里应用于版本号的更新,也就是每次更新的时候,需要比较期望值和当前值,以及期望版本号和当前版本号

    /**
     * 基于CAS引出ABA问题并采用AtomicStampedReference解决
     */
    public class ABADemo {
    
        static AtomicReference<Integer> atomicReference = new AtomicReference<Integer>(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, 127);
                atomicReference.compareAndSet(127, 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, 2020) + "	" + atomicReference.get());
    
            }, "t2").start();
    
            //main线程和gc线程之外如果还有线程就处于等待
            while (Thread.activeCount() > 2) {
                Thread.yield();
            }
            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, 127, atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);
    
                System.out.println(Thread.currentThread().getName() + "	 第二次版本号" + atomicStampedReference.getStamp());
    
                atomicStampedReference.compareAndSet(127, 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, 2020, 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在进行操作的时候,就出现操作失败了,因为版本号和当初拿到的不一样

  • 相关阅读:
    FMDB使用的一点心得:数据库创建、制表、查询等以及image转换成二进制nsdata保存到数据库中
    Java基本数据类型
    hashtable C++实现
    Libgdx中TextButton的一些思考
    [伯努利数] poj 1707 Sum of powers
    POJ 3020:Antenna Placement(无向二分图的最小路径覆盖)
    flume 读取kafka 数据
    [R] 之 帮助函数
    [python] 之 类编码细节
    [python] 之 装饰器
  • 原文地址:https://www.cnblogs.com/bbgs-xc/p/12774199.html
Copyright © 2020-2023  润新知