Java面试题(六) 解决CAS大美女引来的ABA渣男

一. ABA问题简介

假设有线程1和线程2,线程1执行一次任务需要10毫秒,线程2执行一次任务需要2毫秒

线程1先从主内存取出A,但是他慢,他的等,
线程2同时也从内存取出A,并且线程2执行的快,所以他可以进行多次访问主存并且修改主存内的值,
如线程2把A修改成B,在把B修改成A,
当线程1在执行的时候发现主存还是A,他就正常修改数据了。

但是所有人都知道这是有猫腻的,看似一样,实则已经被更换过了,
也可以简单理解为"狸猫换太子"。

代码演示ABA问题

	//原子引用,设置主内存为10
    public static AtomicReference<Integer> atomicReference =
            new AtomicReference<>(10);
            
 	public static void main(String[] args) {

        System.out.println("-------------------下面是ABA问题演示---------------------");
        //下面 2个线程-模拟 ABA问题
        //T1线程
        new Thread(() -> {
            //模拟 ABA
            atomicReference.compareAndSet(10, 11);
            atomicReference.compareAndSet(11, 10);
        }, "T1").start();

        //T2线程
        new Thread(() -> {

            //先暂停 1秒,保证 T1线程先执行一次 ABA操作
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }

            //执行 CAS
            System.out.println(atomicReference.compareAndSet(10, 12) + "\t" + atomicReference.get());

        }, "T2").start();
    }

运行结果
在这里插入图片描述

解析

从结果来看,ABA问题成立,T2线程成功改变主内存的值。

二. 解决ABA问题

需要引入一个新的概念,时间戳原子引用。

简单来说就是当前线程去主内存执行一次后,会在后面加上一个时间戳,
也可以理解成是一个版本号。

如我上面所举的例子,
线程2从主内存取出A,时间戳就变成了1,
在把A修改成B,时间戳就变成了2,
在把B变成A,时间戳就变成了3。
而这时线程1醒了,开始执行任务,一比较,值是对的,但是版本号不对。
这时就提示线程1失败,需要重新去主内存取数据和取时间戳(版本号)。

这样就解决了ABA问题。

代码加注释

	//时间戳原子引用
    //第一个参数是初始值,第二个是初始版本号时间戳
    public static AtomicStampedReference<Integer> stampedReference =
            new AtomicStampedReference<>(10, 1);
            
   	public static void main(String[] args) {

        System.out.println("-------------------下面是解决ABA问题---------------------");

        //T3线程
        new Thread(() -> {

            //获取时间戳(初始版本号)
            int stamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t 第一次版本号: " + stamp);

            //暂停 1秒 T3线程,保证 T4线程能得到主存中第一版的版本号
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }

            //开始 ABA操作
            stampedReference.compareAndSet(10, 11, stampedReference.getStamp(), stampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "\t 第二次版本号: " + stampedReference.getStamp());

            stampedReference.compareAndSet(11, 10, stampedReference.getStamp(), stampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "\t 第三次版本号: " + stampedReference.getStamp());

        }, "T3").start();

        //T4线程
        new Thread(() -> {

            //获取时间戳(初始版本号)
            int stamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t 第一次版本号: " + stamp);

            //暂停 3秒,保证 T3执行一次ABA。
            try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }

            //执行 CAS
            boolean b = stampedReference.compareAndSet(10, 12, stamp, stamp + 1);

            System.out.println(Thread.currentThread().getName() + "\t 修改成功? " + b + "\t 当前版本: " + stampedReference.getStamp());
            System.out.println(Thread.currentThread().getName() + "\t 主存最新值: " + stampedReference.getReference());

        }, "T4").start();
    }

运行结果
在这里插入图片描述

解析

从运行结果可以看出T4线程修改失败,成功解决了ABA问题。
总结:解决ABA问题关键在于,时间戳的引入,这个需要好好理解一下。

猜你喜欢

转载自blog.csdn.net/w_x_A__l__l/article/details/106615173