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