什么是ABA问题

1、ABA问题描述

在多线程场景下CAS会出现ABA问题,关于ABA问题这里简单科普下,例如有2个线程同时对同一个值(初始值为A)进行CAS操作,这三个线程如下:

线程1,期望值为A,欲更新的值为B
线程2,期望值为A,欲更新的值为B

线程1抢先获得CPU时间片,而线程2因为其他原因阻塞了,线程1取值与期望的A值比较,发现相等然后将值更新为B,然后这个时候出现了线程3,期望值为B,欲更新的值为A,线程3取值与期望的值B比较,发现相等则将值更新为A,此时线程2从阻塞中恢复,并且获得了CPU时间片,这时候线程2取值与期望的值A比较,发现相等则将值更新为B,虽然线程2也完成了操作,但是线程2并不知道值已经经过了A->B->A的变化过程。

2、解决方法

要解决ABA问题,可以增加一个版本号,当内存位置V的值每次被修改后,版本号都加1。

2.1、通过AtomicStampedReference来解决ABA问题

    1)AtomicStampedReference内部维护了对象值和版本号,在创建AtomicStampedReference对象时,需要传入初始值和初始版本号;
    2)当AtomicStampedReference设置对象值时,对象值以及状态戳都必须满足期望值,写入才会成功。

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

public static void main(String[] args) {
    // 第一个线程
    new Thread(() -> {
        int stamp = asr.getStamp();
        System.out.println("t1线程拿到的初始版本号:" + stamp);
        
        // 睡眠1秒,是为了让t2线程也拿到同样的初始版本号
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
        }
        
        System.out.println("t1线程第一次compareAndSet结果:" + asr.compareAndSet(100,101,stamp,stamp+1));
        System.out.println("t1线程第二次compareAndSet结果:" + asr.compareAndSet(101,100,stamp+1,stamp+2));
    },"t1").start();
    
    // 第二个线程
    new Thread(() -> {
        int stamp = asr.getStamp(); // 线程t2第一次获取版本号
        System.out.println("t2线程拿到的初始版本号:" + stamp);
        
        // 睡眠3秒,是为了让t1线程完成ABA操作
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
        }
        
        System.out.println("最新版本号:" + asr.getStamp()); // 线程t2重新获取版本号,看版本号是否变了
        // 下面compareAndSet()第三个参数仍传第一次获取的版本号,如果版本号变了,则更新失败
        System.out.println("t2线程compareAndSet结果:" + asr.compareAndSet(100,200,stamp,asr.getStamp()+1) 
                + ", 当前值:" + asr.getReference());
    },"t2").start();
}

  分析

1、初始值100,初始版本号1
2、线程t1和t2拿到一样的初始版本号
3、线程t1完成ABA操作,版本号递增到3
4、线程t2完成CAS操作,最新版本号已经变成3,跟线程t2之前拿到的版本号1不相等,操作失败

  执行结果

t1线程拿到的初始版本号:1
t2线程拿到的初始版本号:1
t1线程第一次compareAndSet结果:true
t1线程第二次compareAndSet结果:true
最新版本号:3
t2线程compareAndSet结果:false, 当前值:100

2.2、通过AtomicMarkableReference解决ABA问题

AtomicStampedReference可以给引用加上版本号,追踪引用的整个变化过程,如:A -> B -> C -> D -> A,通过AtomicStampedReference,我们可以知道,引用变量中途被更改了3次。但是,有时候,我们并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了AtomicMarkableReference,AtomicMarkableReference的唯一区别就是不再用int标识引用,而是使用boolean变量——表示引用变量是否被更改过。

private static AtomicMarkableReference<Integer> amr = new AtomicMarkableReference<>(100,false);

public static void main(String[] args) {
    // 第一个线程
    new Thread(() -> {
        boolean isMarked = amr.isMarked();
        System.out.println("t1线程版本号是否被更改:" + isMarked);
        
        // 睡眠1秒,是为了让t2线程也拿到同样的初始版本号
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
        }
        
        System.out.println("t1线程第一次compareAndSet结果:" + amr.compareAndSet(100,101,isMarked,true));
        System.out.println("t1线程第二次compareAndSet结果:" + amr.compareAndSet(101,100,amr.isMarked(),true));
    },"t1").start();
    
    // 第二个线程
    new Thread(() -> {
        boolean isMarked = amr.isMarked();
        System.out.println("t2版本号是否被更改:" + isMarked);
        
        // 睡眠3秒,是为了让t1线程完成ABA操作
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
        }
        System.out.println("是否更改过:" + amr.isMarked());
        System.out.println("t2线程compareAndSet结果:" + amr.compareAndSet(100,200,isMarked,true) 
                + ", 当前值:" + amr.getReference());
    },"t2").start();
}

  执行结果

t1线程版本号是否被更改:false
t2版本号是否被更改:false
t1线程第一次compareAndSet结果:true
t1线程第二次compareAndSet结果:true
是否更改过:true
t2线程compareAndSet结果:false, 当前值:100
原文地址:https://www.cnblogs.com/xy-ouyang/p/15238282.html