什么是ABA问题
在多线程场景下CAS(比较并交换)会出现ABA问题,比如当有两个线程同时去修改变量的值,线程1和线程2都将变量由A改为B。首先线程1获得CPU的时间片,线程2由于某些原因被挂起,线程1经过CAS(Comparent and Swap)将变量的值从A改为B,线程1更新完变量的值后,此时恰好有线程3进来了,线程3通过CAS(comparent And Swap)将变量的值由B改为A,线程3更新完成后,线程2获取时间片继续执行,通过CAS(comparent And Swap)将变量的值由A改为B,而此时的线程2并不知道该变量已经有了A->B->A改变的过程。这就是CAS中的ABA问题。
如何避免ABA问题
- 通过加版本号(每次变量更新的时候,将当前变量的版本号加1)或者时间戳解决,或者保证单向递增或递减不会存在此类问题。Atmoic包下的
AtmoicStampedReference类,其compareAndSet()方法首先检查当前引用是否等于预期引用,并且当前版本号是否等于预期版本号,如果全部相等,则以原子方式将当前引用更新为新引用(AtomicMarkableReference也可以用来解决CAS中的ABA问题,与AtomicStampReference不同的是其版本号的数据类型为boolean)。
ABA问题再现
使用AtomicInteger无法发现ABA问题
package com.gjy.demo.Atomic;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @Author GJY
* @Date 2021/7/27 20:35
* @Version 1.0
*/
public class AtomicInteger_ABADemo {
private static AtomicInteger at = new AtomicInteger(1);
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
int i = at.get();
System.out.println("线程A读取初始值:"+i);//1
//比较并交换,将at的值从1修改为2
at.compareAndSet(at.get(),2);
//比较并交换,将at的值从2修改为1
at.compareAndSet(at.get(),1);
}
}, "A线程");
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
int i = at.get();
System.out.println("线程B读取初始值:"+i);//1
try {
//线程B调用线程A的join方法,线程B会阻塞直到线程A执行完毕。
threadA.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
//比较并交换,将at的值由1修改回2
boolean result = at.compareAndSet(i, 2);
System.out.println("线程B比较并修改at的值的执行结果-> "+result);//true
}
}, "B线程");
//启动线程
threadA.start();
threadB.start();
}
}
运行结果:
线程A读取初始值:1
线程B读取初始值:1
线程B比较并修改at的值的执行结果-> true
使用AtomicStampReference可以发现ABA问题
package com.gjy.demo.Atomic;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* @Author GJY
* @Date 2021/7/27 20:48
* @Version 1.0
*/
public class AtomicStampReference_ABADemo {
//初始版本号为0
private static AtomicStampedReference<Integer> asr = new AtomicStampedReference<Integer>(1, 0);
public static void main(String[] args) {
Thread threadA = new Thread(() -> {
try {
//线程A睡眠1秒,此时睡眠期间线程B会获得初始版本号0
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取版本号
System.out.println("线程A执行前获取的版本号:" + asr.getStamp());
//线程A比较期望值和版本号并将asr从1改为2
asr.compareAndSet(1, 2, asr.getStamp(), asr.getStamp() + 1);
//线程A比较期望值和版本号并将asr从2改回为1
asr.compareAndSet(2, 1, asr.getStamp(), asr.getStamp() + 1);
System.out.println("线程A执行两次修改后的版本号:" + asr.getStamp());
}, "线程A");
Thread threadB = new Thread(() -> {
//线程B获取版本号0,因为线程A开始执行时先睡眠1秒,然后再执行比较并交换
int stamp = asr.getStamp();
System.out.println("线程B获取的版本号:"+stamp);//0
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//线程B比较期望值和版本号并将asr从1改为2
boolean result = asr.compareAndSet(1,
2,
stamp,
stamp + 1);
System.out.println("线程B比较并修改asr的值的执行结果-> "+result);
System.out.println("线程B执行CAS后的版本号:"+asr.getStamp());//2
}, "线程B");
threadA.start();
threadB.start();
}
}
运行结果:
线程B获取的版本号:0
线程A执行前获取的版本号:0
线程A执行两次修改后的版本号:2
线程B比较并修改asr的值的执行结果-> false
线程B执行CAS后的版本号:2
AtomicStampReference源码
AtomicStampReference类的属性
//volatile修饰的pair
private volatile Pair<V> pair;
AtomicStampReference类的构造方法
/**
* V initialRef :任意类型的初始引用对象
* int initialStamp :Int类型的初始版本号
*/
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}
AtomicStampReference的内部类Pair
- AtomicStampReference的内部类Pair维护了带有版本的Pair对象,Pair源码如下:
private static class Pair<T> {
//引用
final T reference;
//版本号
final int stamp;
//构造方法
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
//生成一个Pair
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
comparedAndSwap()方法
/**
* @param expectedReference 期望的引用值
* @param newReference 新的引用值
* @param expectedStamp 期望的版本号
* @param newStamp 新的版本号
* @return {@code true} if successful 更新成功返回true,失败返回false
*/
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference && newStamp == current.stamp)
||
casPair(current, Pair.of(newReference, newStamp))
);
}
- 具体执行流程:
- 若原引用不等于期望引用,则方法返回false,否则执行第二步
- 若原引用等于新引用并且原版本号与期望版本号不一致,则返回false,否则执行第三步
- 若原引用与新引用相等并且原版本号与新版本号一致,则无需修改引用和版本号,否则,调用casPair()方法进行原子性更新引用和版本号。
以上对ABA问题、AtomicStampReference的简单介绍。在我们的业务开发中经常会出现ABA问题类似的场景,但又往往不容易被我们开发者所发现,往往等到测试的时候才被发现,所以正确认识ABA问题、掌握解决ABA问题的方法,可以减少我们业务的Bug。
往期推荐