原子類AtomicInteger的ABA問題
連環套路
從AtomicInteger引出下面的問題
CAS -> Unsafe -> CAS底層思想 -> ABA -> 原子引用更新 -> 如何規避ABA問題
ABA問題是什么
狸貓換太子
假設現在有兩個線程,分別是T1 和 T2,然后T1執行某個操作的時間為10秒,T2執行某個時間的操作是2秒,最開始AB兩個線程,分別從主內存中獲取A值,但是因為B的執行速度更快,他先把A的值改成B,然后在修改成A,然后執行完畢,T1線程在10秒后,執行完畢,判斷內存中的值為A,並且和自己預期的值一樣,它就認為沒有人更改了主內存中的值,就快樂的修改成B,但是實際上 可能中間經歷了 ABCDEFA 這個變換,也就是中間的值經歷了狸貓換太子。
所以ABA問題就是,在進行獲取主內存值的時候,該內存值在我們寫入主內存的時候,已經被修改了N次,但是最終又改成原來的值了
CAS導致ABA問題
CAS算法實現了一個重要的前提,需要取出內存中某時刻的數據,並在當下時刻比較並替換,那么這個時間差會導致數據的變化。
比如說一個線程one從內存位置V中取出A,這時候另外一個線程two也從內存中取出A,並且線程two進行了一些操作將值變成了B,然后線程two又將V位置的數據變成A,這時候線程one進行CAS操作發現內存中仍然是A,然后線程one操作成功
盡管線程one的CAS操作成功,但是不代表這個過程就是沒有問題的
ABA問題
CAS只管開頭和結尾,也就是頭和尾是一樣,那就修改成功,中間的這個過程,可能會被人修改過
原子引用
原子引用其實和原子包裝類是差不多的概念,就是將一個java類,用原子引用類進行包裝起來,那么這個類就具備了原子性
/**
* 原子類引用
*/
@Data
@AllArgsConstructor
class User {
String userName;
int age;
}
public class AtomicReferenceDemo {
public static void main(String[] args) {
User aaa = new User("aaa", 20);
User bbb = new User("bbb", 30);
// 創建原子引用包裝類
AtomicReference<User> atomicReference = new AtomicReference<>();
// 現在主物理內存的共享變量,為aaa
atomicReference.set(aaa);
// 比較並交換,如果現在主物理內存的值為aaa,那么交換成bbb
System.out.println(atomicReference.compareAndSet(aaa, bbb) + "\t " + atomicReference.get().toString());
// 比較並交換,現在主物理內存的值是bbb了,但是預期為aaa,因此交換失敗
System.out.println(atomicReference.compareAndSet(aaa, bbb) + "\t " + atomicReference.get().toString());
}
}
基於原子引用的ABA問題
我們首先創建了兩個線程,然后T1線程,執行一次ABA的操作,T2線程在一秒后修改主內存的值
/**
* 基於CAS引出ABA問題
*/
public class ABADemo {
static AtomicReference<Integer> atomicReference = new AtomicReference<Integer>(100);
public static void main(String[] args) {
new Thread(()->{
// 把100 改成 127 然后在改成100,也就是ABA
atomicReference.compareAndSet(100, 127);
//特別強調在AtomicReference(Integer)中value超出-128~127,會生成一個新的對象而造成無法修改
//但是在AtomicInteger中則不會存在這樣的問題
atomicReference.compareAndSet(127, 100);
},"t1").start();
new Thread(()->{
try {
// 睡眠一秒,保證t1線程,完成了ABA操作
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 把100 改成 127 然后在改成100,也就是ABA
System.out.println(atomicReference.compareAndSet(100, 2021)+"\t"+atomicReference.get());
},"t2").start();
}
}
我們發現,它能夠成功的修改,這就是ABA問題
解決ABA問題
新增一種機制,也就是修改版本號,類似於時間戳的概念
T1: 100 1 2020 2
T2: 100 1 127 2 100 3
如果T1修改的時候,版本號為2,落后於現在的版本號3,所以要重新獲取最新值,這里就提出了一個使用時間戳版本號,來解決ABA問題的思路
AtomicStampedReference
時間戳原子引用,來這里應用於版本號的更新,也就是每次更新的時候,需要比較期望值和當前值,以及期望版本號和當前版本號
/**
* 基於CAS引出ABA問題並采用AtomicStampedReference解決
*/
public class ABADemo {
static AtomicReference<Integer> atomicReference = new AtomicReference<Integer>(100);
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
public static void main(String[] args) {
System.out.println("============以下是ABA問題的產生==========");
new Thread(() -> {
// 把100 改成 101 然后在改成100,也就是ABA
atomicReference.compareAndSet(100, 127);
atomicReference.compareAndSet(127, 100);
}, "t1").start();
new Thread(() -> {
try {
// 睡眠一秒,保證t1線程,完成了ABA操作
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 把100 改成 101 然后在改成100,也就是ABA
System.out.println(atomicReference.compareAndSet(100, 2020) + "\t" + atomicReference.get());
}, "t2").start();
//main線程和gc線程之外如果還有線程就處於等待
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println("============以下是ABA問題的解決==========");
new Thread(() -> {
// 獲取版本號
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t 第一次版本號" + stamp);
// 暫停t3一秒鍾
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 傳入4個值,期望值,更新值,期望版本號,更新版本號
atomicStampedReference.compareAndSet(100, 127, atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName() + "\t 第二次版本號" + atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(127, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName() + "\t 第三次版本號" + atomicStampedReference.getStamp());
}, "t3").start();
new Thread(() -> {
// 獲取版本號
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t 第一次版本號" + stamp);
// 暫停t4 3秒鍾,保證t3線程也進行一次ABA問題
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();
}
}
運行結果為:
我們能夠發現,線程t3,在進行ABA操作后,版本號變更成了3,而線程t4在進行操作的時候,就出現操作失敗了,因為版本號和當初拿到的不一樣