ABA问题是CAS机制中出现的一个问题,这里先说明一下CAS和原子操作:
CAS
Compare And Swap,比较并交换。是java.util.concurrent包实现的区别于synchronized同步锁的一种乐观锁。CAS有三个操作数,内存值V,预期值A,要修改的值B,当且仅当内存值V与预期值A相等时,将内存值V修改为B,否则什么也不做。
原子操作
“原子”代表最小的执行单位,该操作在执行完毕前不会被任何其他任务或者事件打断。
AtomicInteger类的compareAndSet通过原子操作实现了CAS操作,最底层基于汇编语言实现。
1.什么是ABA问题?
一个线程把数据A变成了B,然后又重新变成了A,此时另一个线程读取该数据的时候,发现A没有变化,就误认为是原来的那个A,但是此时A的一些属性或状态已经发生过变化。
下面一段代码对ABA问题进行重现:
package com.lpl;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class ABA {
private static AtomicInteger index = new AtomicInteger(10);
public static void main(String[] args) {
//张三线程去修改index的值
new Thread(() -> {
//通过CAS自旋算法锁修改index的值
index.compareAndSet(10, 11);
index.compareAndSet(11, 10);
System.out.println(Thread.currentThread().getName() + " 10 -> 11 -> 10");
}, "张三").start();
//李四线程去读取内存值并设置新值
new Thread(() -> {
try{
//线程休眠2秒
TimeUnit.SECONDS.sleep(2);
//判断是否修改成功
boolean isSuccess = index.compareAndSet(10, 12);
System.out.println(Thread.currentThread().getName() + " index是否是预期的值:" + isSuccess + ",设置的新值是:" + index.get());
}catch (InterruptedException e) {
e.printStackTrace();
}
}, "李四").start();
}
}
程序运行的结果为:
这里正常情况下在张三对index进行了操作后虽然index的值没有发生变化,但是李四再次拿到并进行操作的数据已经不是原来最初的数据了,这就产生了ABA问题。
2.解决方案
要解决ABA问题,可以增加一个版本号,当内存位置V的值每次被修改后,版本号都加1。
(1)第一种方式,使用AtomicStampedReference对象
AtomicStampedReference内部维护了对象值和版本号,在创建AtomicStampedReference对象时,需要传入初始值和初始版本号,当AtomicStampedReference设置对象值时,对象值以及状态戳都必须满足期望值,写入才会成功。
对上面程序进行修改:
package com.lpl;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABA {
private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(10, 1);
public static void main(String[] args) {
//张三线程去修改参考对象的值
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 拿到的当前时间戳版本号为:" + atomicStampedReference.getStamp());
//休眠1秒,为了让李四线程也拿到同样的初始版本号
try{
TimeUnit.SECONDS.sleep(1);
}catch (InterruptedException e) {
e.printStackTrace();
}
//通过CAS自旋算法锁修改index的值
atomicStampedReference.compareAndSet(10, 11, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
atomicStampedReference.compareAndSet(11, 10, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + " 10 -> 11 -> 10");
}, "张三").start();
//李四线程去读取内存值并设置新值
new Thread(() -> {
try{
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + " 拿到的当前时间戳版本号为:" + stamp);
//线程休眠2秒,为了让张三线程完成ABA操作
TimeUnit.SECONDS.sleep(2);
//判断是否修改成功
boolean isSuccess = atomicStampedReference.compareAndSet(10, 12, stamp, atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + " 最新版本号:" + atomicStampedReference.getStamp() + ",是否修改成功:" + isSuccess + ",当前值是:" + atomicStampedReference.getReference());
}catch (InterruptedException e) {
e.printStackTrace();
}
}, "李四").start();
}
}
程序运行结果:
线程张三完成CAS操作,最新版本号已经变成3,与线程李四之前拿到的版本号1不相等,所以操作失败。
有时候,我们并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了AtomicMarkableReference对象,可以标识引用变量是否被更改过。
(2)第二种方式,使用AtomicMarkableReference对象
修改代码为:
package com.lpl;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicMarkableReference;
public class ABA {
private static AtomicMarkableReference<Integer> atomicMarkableReference = new AtomicMarkableReference<Integer>(10, false);
public static void main(String[] args) {
//张三线程去修改参考对象的值
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 当前参考对象是否被修改:" + atomicMarkableReference.isMarked());
//休眠1秒,为了让李四线程也拿到是否被修改的标识
try{
TimeUnit.SECONDS.sleep(1);
}catch (InterruptedException e) {
e.printStackTrace();
}
//通过CAS自旋算法锁修改index的值
atomicMarkableReference.compareAndSet(10, 11, atomicMarkableReference.isMarked(), true);
atomicMarkableReference.compareAndSet(11, 10, atomicMarkableReference.isMarked(), true);
System.out.println(Thread.currentThread().getName() + " 10 -> 11 -> 10");
}, "张三").start();
//李四线程去读取内存值并设置新值
new Thread(() -> {
try{
boolean isMarked = atomicMarkableReference.isMarked();
System.out.println(Thread.currentThread().getName() + " 当前参考对象是否被修改:" + isMarked);
//线程休眠2秒,为了让张三线程完成ABA操作
TimeUnit.SECONDS.sleep(2);
//判断是否修改成功
boolean isSuccess = atomicMarkableReference.compareAndSet(10, 12, isMarked, true);
System.out.println(Thread.currentThread().getName() + " 当前修改状态为:" + atomicMarkableReference.isMarked() + ",是否修改成功:" + isSuccess + ",当前值是:" + atomicMarkableReference.getReference());
}catch (InterruptedException e) {
e.printStackTrace();
}
}, "李四").start();
}
}
程序运行结果为:
由于张三线程执行了ABA操作,李四线程一开始拿到的预期修改状态与操作时内存中的修改状态不一致导致操作失败,避免了ABA问题。