上一篇写CAS的时候,我列举了它的三个缺点,我们先来复习一下:
CAS的缺点:
-
循环时间长,开销大(主要原因:do{}while()可能无限循环)
-
只能保证一个共享变量的原子操作,如果是多个共享变量的话,就需要加锁来保证原子性。
-
会引出ABA问题。
那么这一篇我们来说一下什么是ABA问题
ABA问题(狸猫换太子):
假设现在有T1和T2两个线程,T1线程的执行时间为10秒,T2线程的执行时间为2秒。程序执行时,T1和T2都从主物理内存中拷贝一份值到自己的工作内存,由于T2线程的执行速度太快,T2先将A改为了B,然后写回主物理内存,之后又将B改为A,再次写回主物理内存,这个时候T1线程是不知道的,因为它的执行速度太慢。那么在T1线程执行完毕后,它会判断主物理内存中的值和自己的期望值是不是一样的,它发现A=A,是一样的,所以T1认为没有人修改过主物理内存中的值,它会继续它的操作。但事实上,中间经过了ABA的一种变化,只是T1不知道而已,这就是所谓的ABA问题。
CAS引出的ABA问题:CAS只管开头和结尾,如果头尾一致,它就认为没人修改过,但实际上,中间可能被人狸猫换太子了。
那么其实ABA问题也是基于原子引用的,所以我们先来说一下原子引用。
原子引用:
将一个java类用原子引用类包装起来,这个类就具备了原子性。
代码示例:
public class AtomicReferenceDemo {
public static void main(String[] args) {
User z3 = new User("z3",22);
User li4 = new User("li4",25);
AtomicReference<User> atomicReference = new AtomicReference<>();
//设置主物理内存的共享变量是z3
atomicReference.set(z3);
System.out.println(atomicReference.compareAndSet(z3,li4) + "\t" + atomicReference.get().toString());
System.out.println(atomicReference.compareAndSet(z3,li4) + "\t" + atomicReference.get().toString());
}
}
class User{
String userName;
int age;
@Override
public String toString() {
return "userName:"+userName + "\tage:" + age;
}
public String getUserName() {
return userName;
}
public int getAge() {
return age;
}
public User(String userName, int age) {
this.userName = userName;
this.age = age;
}
}
运行结果:
基于原子应用的ABA问题:
public class ABADemo {
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
public static void main(String[] args) {
new Thread(()->{
atomicReference.compareAndSet(100,101);
atomicReference.compareAndSet(101,100);
},"t1").start();
new Thread(()->{
//让t2休眠1秒,保证t1线程完成ABA操作
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicReference.compareAndSet(100, 2020);
System.out.println(Thread.currentThread().getName() + "\t"+atomicReference.get());
},"t2").start();
}
}
运行结果:
t2线程修改成功了,这就是ABA问题。
解决ABA问题:
怎么解决ABA问题呢?可以通过修改版本号的方式解决ABA问题。
这种方式类似于时间戳的概念。
AtomicStampedReference
时间戳的原子引用,每次更新的时候,需要比较期望值和当前值,以及期望版本号和当前版本号。
代码示例:
public class ABADemo { //ABA问题的解决 AtomicStampedReference
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100,1);
public static void main(String[] args) {
System.out.println("==================以下是ABA问题的产生======================");
new Thread(()->{
atomicReference.compareAndSet(100,101);
atomicReference.compareAndSet(101,100);
},"t1").start();
new Thread(()->{
try {
//暂停1秒钟t2线程,保证上面的t1线程完成了一次ABA操作
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicReference.compareAndSet(100, 2020) + "\t" + atomicReference.get());
},"t2").start();
//暂停一会线程
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("===============以下是ABA问题的解决========================");
new Thread(()->{
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t第1次版本号:"+stamp);
//暂停1秒钟t3线程
try {
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第2次版本号:"+atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName() + "\t第3次版本号:"+atomicStampedReference.getStamp());
},"t3").start();
new Thread(()->{
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t第1次版本号:"+stamp);
//暂停3秒钟t4线程
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result = atomicStampedReference.compareAndSet(100, 2020, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName() + "\t修改成功否:"+result+"\t当前最新实际版本号:"+atomicStampedReference.getStamp());
System.out.println(Thread.currentThread().getName() + "\t当前实际最新值:"+atomicStampedReference.getReference());
},"t4").start();
}
}
运行结果:
我们可以发现,在加了版本号之后,ABA的问题解决了,t4线程修改失败。