什么是ABA問題?
簡單來說就是:狸貓換太子。
假設這里有兩個線程線程1和線程2,線程1工作時間需要10秒,線程2工作需要2秒,主內存值為A,第一輪線程1和線程2都把A拿到自己的工作內存,2秒中后線程2工作完成把A改成了B再寫回去,又過了2秒,線程2把B改成了A再寫回去,然后就線程2進入休眠狀態,這時候線程1工作完成,看到期望為A真實值也是A認為沒有人動過,然后線程1進行CAS操作。
ABA問題的產生
CAS會導致ABA問題。
CAS算法實現一個重要前提需要取出內存中某時刻的數據並在當下時刻比較並替換,那么這個時間差會導致數據的變化。
比如說一個線程1從內存位置V中取出A,這時候另一個線程2也從內存位置V中取出A,並且線程2進行了一些操作將值變成了B,然后線程2又將V位置的數據變成A,這時候線程1進行CAS操作發現內存中V的位置仍是A,然后線程1操作成功。盡管線程1的CAS操作成功,但是不代表這個過程就是沒問題的。
ABA問題的解決
新增一種機制,那就是修改版本號(類似於時間戳)。
用AtomicStampedReference(int initialRef, int initialStamp),它有兩個參數,第一個初始值,第二是初始版本號,它的compareAndSet方法有四個參數:
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)));
}
第一個參數期望值,第二個參數跟新值,第三個參數當前版本號,第四個參數最新版本號。
代碼演示:
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100,1);
public static void main(String[] args) {
// 線程1進行一次ABA操作
new Thread(() ->{
atomicReference.compareAndSet(100,101);
atomicReference.compareAndSet(101,100);
},"線程1").start();
new Thread(() ->{
// 線程2先暫停1秒,確保線程1能順利完成一次ABA操作
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicReference.compareAndSet(100,2019)+"\t"+atomicReference.get());
},"線程2").start();
// 以下是ABA問題的解決
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() ->{
// 線程2先暫停1秒,確保線程1能順利完成一次ABA操作
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t 第一次版本號:"+stamp);
// 暫停1秒鍾線程3
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100,101,stamp,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());
},"線程3").start();
new Thread(() ->{
// 線程2先暫停1秒,確保線程1能順利完成一次ABA操作
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t 第一次版本號:"+stamp);
// 暫停3秒鍾線程4,保證線程3完成一次ABA操作
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result = atomicStampedReference.compareAndSet(100,2019,stamp,atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t 修改是否成功:"+result+"\t 版本號:"+atomicStampedReference.getStamp());
System.out.println(Thread.currentThread().getName()+"\t 當前實際最新值:"+atomicStampedReference.getReference());
},"線程4").start();
}
運行結果: