原子類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在進行操作的時候,就出現操作失敗了,因為版本號和當初拿到的不一樣