Java并發(fā)必知必會第三彈:用積木講解ABA原理
掃描二維碼
隨時隨地手機看文章
作者介紹
悟空聊架構(gòu)
種樹比較好的時間是十年前,其次是現(xiàn)在。
自主開發(fā)了Java學(xué)習(xí)平臺、PMP刷題小程序。目前主修Java、
多線程、
SpringBoot、
SpringCloud、
k8s。
本公眾號不限于分享技術(shù),也會分享工具的使用、人生感悟、讀書總結(jié)。
可落地的 Spring Cloud 實戰(zhàn)項目持續(xù)更新中,點擊底部[閱讀原文]獲取。
本篇主要內(nèi)容如下
本篇主要內(nèi)容如下
一、背景
上一節(jié)我們講了程序員深夜慘遭老婆鄙視,原因竟是CAS原理太簡單?,留了一個彩蛋給大家,ABA問題是怎么出現(xiàn)的,為什么不是AAB拖拉機,AAA金花,4個A炸彈 ?這一篇我們再來揭開ABA的神秘面紗。
二、面試連環(huán)炮
面試的時候我們也經(jīng)常遭遇面試官的連環(huán)追問:
-
CAS概念?
-
Unsafe類是干啥用的?
-
CAS底層實現(xiàn)是怎么樣的
-
ABA問題什么場景下會出現(xiàn)?
-
ABA有什么危害?
-
原子引用更新是啥?
-
如何避免ABA問題?
三、用積木講解ABA問題
案例:甲看見一個三角形積木,覺得不好看,想替換成五邊形,但是乙想把積木替換成四邊形。(前提條件,只能被替換一次)
-
第一步:乙先搶到了積木,將
三角形A積木替換成
五角星B1
-
第二步:乙將
五角星B1替換成
五邊形B2
-
第三步:乙將
五邊形B2替換成
棱形B3
-
第四步:乙將
棱形B3替換成
六邊形B4
-
第五步:乙將
六邊形B4替換成
三角形A
-
第六步:甲看到積木還是三角形,認(rèn)為乙沒有替換,甲可以進行替換
-
第七步:甲將
三角形V替換成了
五邊形B
**講解:**第一步到第五步,都是乙在替換,但最后還是替換成了三角形(即是不是同一個三角形),這個就是ABA,A指最開始是三角形,B指中間被替換的B1/B2/B3/B4,第二個A就是第五步中的A,中間不論經(jīng)過怎么樣的形狀替換,最后還是變成了三角形。然后甲再將A2和A1進行形狀比較,發(fā)現(xiàn)都是三角形,所以認(rèn)為乙沒有動過積木,甲可以進行替換。這個就是比較并替換(CAS)中的ABA問題。
**小結(jié):**CAS只管開頭和結(jié)尾,中間過程不關(guān)心,只要頭尾相同,則認(rèn)為可以進行修改,而中間過程很可能被其他人改過。
四、用原子引用類演示ABA問題
AtomicReference:原子引用類
-
1.首先我們需要定義一個積木類
/**
積木類
* @author: 悟空聊架構(gòu)
* @create: 2020-08-25
*/ class BuildingBlock {
String shape; public BuildingBlock(String shape) { this.shape = shape;
} @Override public String toString() { return "BuildingBlock{" + "shape='" + shape + '}';
}
}
-
2.定義3個積木:三角形A,四邊形B,五邊形D
static BuildingBlock A = new BuildingBlock("三角形"); // 初始化一個積木對象B,形狀為四邊形 static BuildingBlock B = new BuildingBlock("四邊形"); // 初始化一個積木對象D,形狀為五邊形 static BuildingBlock D = new BuildingBlock("五邊形");
-
初始化原子引用類
static AtomicReferenceatomicReference = new AtomicReference<>(A);
-
4.線程“乙”執(zhí)行ABA操作
new Thread(() -> {// 初始化一個積木對象A,形狀為三角形 atomicReference.compareAndSet(A, B); // A->B atomicReference.compareAndSet(B, A); // B->A },
-
5.線程“甲”執(zhí)行比較并替換
new Thread(() -> {// 初始化一個積木對象A,形狀為三角形 try { // 睡眠一秒,保證t1線程,完成了ABA操作 TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
} // 可以替換成功,因為乙線程執(zhí)行了A->B->A,形狀沒變,所以甲可以進行替換。 System.out.println(atomicReference.compareAndSet(A, D) + "\t" + atomicReference.get()); // true BuildingBlock{shape='五邊形} }, "甲").start();
**輸出結(jié)果:**true BuildingBlock{shape='五邊形}
**小結(jié):**當(dāng)線程“乙”執(zhí)行ABA之后,線程“甲”比較后,發(fā)現(xiàn)預(yù)期值和當(dāng)前值一致,將三角形替換成了五邊形。
五、那ABA到底有什么危害?
我們看到乙不管怎么進行操作,甲看到的還是三角形,那甲當(dāng)成乙沒有改變積木形狀 又有什么問題呢?
出現(xiàn)的問題場景通常是帶有消耗類的場景,比如庫存減少,商品賣出。
1.我們想象一下生活中的這個喝水場景:
(1)一家三口人,爸爸、媽媽、兒子。
(2)一天早上6點,媽媽給兒子的水杯灌滿了水(水量為A),兒子先喝了一半(水量變成B)。
(3)然后媽媽把水杯又灌滿了(水量為A),等中午再喝(媽媽執(zhí)行了一個ABA操作)。
(4)爸爸7點看到水杯還是滿的(不知道是媽媽又灌滿的),于是給兒子喝了1/3(水量變成D)
(5)那在中午之前,兒子喝了1/2+1/3= 5/6的水,這不是媽媽期望的,因為媽媽只想讓兒子中午之前喝半杯水。
這個場景的ABA問題帶來的后果就是本來只用喝1/2的水,結(jié)果喝了5/6的水。
2.我們再想象一下電商中的場景
(1)商品Y的庫存是10(A)
(2)用戶m購買了5件(B)
(3)運營人員乙補貨5件(A)(乙執(zhí)行了一個ABA操作)
(4)運營人員甲看到庫存還是10,就認(rèn)為一件也沒有賣出去(不考慮交易記錄),其實已經(jīng)賣出去了5件。
那我們怎么解決原子引用的問題呢?
可以用加版本號的方式來解決兩個A相同的問題,比如上面的積木案例,我們可以給兩個三角形都打上一個版本號的標(biāo)簽,如A1和A2,在第六步中,形狀和版本號一致甲才可以進行替換,因形狀都是三角形,而版本號一個1,一個是2,所以不能進行替換。
在Java代碼中,我們可以用原子時間戳引用類型:AtomicStampedReference
六、帶版本號的原子引用類型
1.我們看一看這個原子類AtomicStampedReference的底層代碼
比較并替換方法compareAndSet
public boolean compareAndSet(V expectedReference,
V newReference, int expectedStamp, int newStamp) {
Paircurrent = pair; return expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
expectedReference:期望值
newReference:替換值
expectedStamp:期望版本號
newStamp:替換版本號
先比較期望值expectedReference和當(dāng)前值是否相等,以及期望版本號和當(dāng)前版本號是否相等,如果兩者都相等,則表示沒有被修改過,可以進行替換。
2.如何使用AtomicStampedReference?
(1)先定義3個積木:三角形A,四邊形B,五邊形D
// 初始化一個積木對象A,形狀為三角形 BuildingBlock A = new BuildingBlock("三角形"); // 初始化一個積木對象B,形狀為四邊形,乙會將三角形替換成四邊形 BuildingBlock B = new BuildingBlock("四邊形"); // 初始化一個積木對象B,形狀為四邊形,乙會將三邊形替換成五邊形 BuildingBlock D = new BuildingBlock("五邊形");
(2)創(chuàng)建一個原子引用類型的實例 atomicReference
// 傳遞兩個值,一個是初始值,一個是初始版本號 AtomicStampedReferenceatomicStampedReference = new AtomicStampedReference<>(A, 1);
(3)創(chuàng)建一個線程“乙”執(zhí)行ABA操作
new Thread(() -> { // 獲取版本號 int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t 第一次版本號" + stamp); // 暫停線程“乙”1秒鐘,使線程“甲”可以獲取到原子引用的版本號 try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
} /*
* 乙線程開始ABA替換
* */ // 1.比較并替換,傳入4個值,期望值A(chǔ),更新值B,期望版本號,更新版本號 atomicStampedReference.compareAndSet(A, B, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t 第二次版本號" + atomicStampedReference.getStamp()); //乙 第一次版本號1 // 2.比較并替換,傳入4個值,期望值B,更新值A(chǔ),期望版本號,更新版本號 atomicStampedReference.compareAndSet(B, A, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1); // 乙 第二次版本號2 System.out.println(Thread.currentThread().getName() + "\t 第三次版本號" + atomicStampedReference.getStamp()); // 乙 第三次版本號3 }, "乙").start();
1)乙先獲取原子類的版本號,第一次獲取到的版本號為1
2)暫停線程“乙”1秒鐘,使線程“甲”可以獲取到原子引用的版本號
3)比較并替換,傳入4個值,期望值A(chǔ),更新值B,期望版本號stamp,更新版本號stamp+1。A被替換為B,當(dāng)前版本號為2
4)比較并替換,傳入4個值,期望值B,更新值A(chǔ),期望版本號getStamp(),更新版本號getStamp()+1。B替換為A,當(dāng)前版本號為3
(4)創(chuàng)建一個線程“甲”執(zhí)行D替換A操作
new Thread(() -> { // 獲取版本號 int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t 第一次版本號" + stamp); // 甲 第一次版本號1 // 暫停線程“甲”3秒鐘,使線程“乙”進行一次ABA替換操作 try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
} boolean result = atomicStampedReference.compareAndSet(A,D,stamp,stamp + 1);
System.out.println(Thread.currentThread().getName() + "\t 修改成功否" + result + "\t 當(dāng)前最新實際版本號:" + atomicStampedReference.getStamp()); // 甲 修改成功否false 當(dāng)前最新實際版本號:3 System.out.println(Thread.currentThread().getName() + "\t 當(dāng)前實際最新值:" + atomicStampedReference.getReference()); // 甲 當(dāng)前實際最新值:BuildingBlock{shape='三角形} }, "甲").start();
(1)甲先獲取原子類的版本號,版本號為1,因為乙線程還未執(zhí)行ABA,所以甲獲取到的版本號和乙獲取到的版本號一致。
(2)暫停線程“甲”3秒鐘,使線程“乙”進行一次ABA替換操作
(3)乙執(zhí)行完ABA操作后,線程甲執(zhí)行比較替換,期望為A,實際是A,版本號期望值是1,實際版本號是3
(4)雖然期望值和實際值都是A,但是版本號不一致,所以甲不能將A替換成D,這個就避免了ABA的問題。
小結(jié): 帶版本號的原子引用類可以利用CAS+版本號來比較變量是否被修改。
總結(jié)
本篇分析了ABA產(chǎn)生的原因,然后又列舉了生活中的兩個案例來分析ABA的危害。然后提出了怎么解決ABA問題:用帶版本號的原子引用類AtomicStampedReference。
限于篇幅和側(cè)重點,CAS的優(yōu)化并沒有涉及到,后續(xù)再倒騰這一塊吧。另外AtomicStampedReference的缺點本篇本沒有進行講解,限于筆者的技術(shù)水平原因,并沒有一一作答,期待后續(xù)能補上這一塊的解答。
我是悟空,一只努力變強的碼農(nóng)!我要變身超級賽亞人啦!
特別推薦一個分享架構(gòu)+算法的優(yōu)質(zhì)內(nèi)容,還沒關(guān)注的小伙伴,可以長按關(guān)注一下:
長按訂閱更多精彩▼
如有收獲,點個在看,誠摯感謝
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!