什么是ABA问题?
ABA问题是由CAS而导致的一个问题
CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并交换,那么在这个时间差内会导致数据的变化。
比如说一个线程一从内存位置V中取出A,这是另一个线程二也从内存中取出A,并且线程二进行类一些操作将值变成了B,然后线程二又将V位置的数据变成A,这时候线程一进行CAS操作发现内存中仍然是A,然后线程一操作成功!
尽管线程一的CAS操作成功,但是不代表这个过程就是没有问题的。
就比如你家每天,都有一个B流浪汉来你家进行生活,他每天在你回家的时候将家整理成你离开的样子,不留下痕迹。你每天回到家后没有发现什么异常于是就正常生活。虽然他目前没有对你的生活造成什么问题,但是这个过程是很有隐患的!!!
总的来说以前的CAS操作,只管过程,也就是开头和结尾是否相同,如果相同就进行操作!而不管你中间到底进行来什么操作!!
原子引用
如何解决ABA问题呢?首先我们看一下原子引用。我们先前已经使用过java.util.concurrent.atomic
下的基本数据类型原子操作类。但是缺少对对象的操作的原子类。也就是AtomicReference
我们来写一个案例来感受一下。
class User{
String userName;
int age;
public User(String userName, int age) {
this.userName = userName;
this.age = age;
}
@Override
public String toString() {
return "Offer2019.User{" +
"userName='" + userName + '\'' +
", age=" + age +
'}';
}
}
public class AtomicReferenceDemo {
public static void main(String[] args) {
User zs = new User("zs", 22);
User ls = new User("ls", 25);
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(zs);
System.out.println(atomicReference.compareAndSet(zs,ls)+"\t"+atomicReference.get().toString());
System.out.println(atomicReference.compareAndSet(zs,ls)+"\t"+atomicReference.get().toString());
}
}
复制代码
atomicReference.set(zs);
讲zs设为当前值
第一个atomicReference.compareAndSet(zs,ls)
是将当前值和预期值比较,两者相同都为zs,所以更新为ls,返回true。
第二个atomicReference.compareAndSet(zs,ls)
将当前值与预期值比较,当前值为ls预期值为zs比较失败,更新失败,返回false。
带时间戳的原子引用
我们将用到另一个新的类AtomicStampedReference
,每次修改值后带有一个版本号!
解决ABA问题
案例
解决ABA问题之前我们先来用代码演示一下什么是ABA问题
public class ABADemo {
//static方便直接使用
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
//ABA问题
public static void main(String[] args) {
new Thread(() -> {
atomicReference.compareAndSet(100, 101);
atomicReference.compareAndSet(101, 100);
}, "t1").start();
new Thread(() -> {
try {
//暂停1秒,保证t1已经完成了一次ABA操作
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicReference.compareAndSet(100, 2020)+"\t"+atomicReference.get());
}, "t2").start();
}
}
复制代码
这就是ABA问题。
解决
解决ABA问题我们就要使用带时间戳的原子引用AtomicStampedReference
。首先查看API
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
首先给定初始值为100,初始版本号为1。
t3线程一开始通过getStamp()
方法拿到目前的初始版本号,后暂停一秒,确保t4线程启动也拿到目前初始的版本号,t4线程启动后暂定3秒,从而保证t3线程的继续执行。t3线程首先通过compareAndSet()
方法将预期值,和预期版本与当前值,当前版本分别对比只有两者都相同才将两值更新成更新的值。t3首先将100更新成101,版本号+1变成2。接着将101又更新成了100,版本号+1变成了3,t3线程完成ABA操作。
此时t4线程被调度执行接着继续执行操作,通过compareAndSet()
将预期值和当前值比较同为100,但是预期版本号为1,当前版本号已经变成了3两者不一致!!所以更新失败!通过每次操作后版本的变化,从而解决了ABA问题的发生。
public class ABADemo {
//static方便直接使用
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
//ABA问题
public static void main(String[] args) {
//ABA问题的解决
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();//版本号
System.out.println(Thread.currentThread().getName() + "\t第一次版本号:" + stamp);
try {
//确保t4线程拿到初始版本号
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t第二次版本号:" + atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t第三次版本号:" + atomicStampedReference.getStamp());
}, "t3").start();
//ABA问题的解决
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t第一次版本号:" + stamp);
try {
//确保t3线程完成ABA操作
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100, 2020, stamp, atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t第二次版本号:" + atomicStampedReference.getStamp() + "\t当前值为:" + atomicStampedReference.getReference());
}, "t4").start();
}
}
复制代码
ABA问题就像一个人总是在你上班的时候闯进你的房子,肆无忌惮的使用房子里的东西,仿佛是房子的主人,却总能在你下班前将房子里的东西还原归位,让你感受不到有人动过的痕迹。虽然看起来相安无事,但是存在很多不可控的因素?身为房子的主人,你也忍受不了这种情况。如何解决呢,就要使用volatile
来保证线程的可见性~