一. 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问题。