號外:可落地的 Spring Cloud項目:PassJavajava
上一節咱們講了程序員深夜慘遭老婆鄙視,緣由竟是CAS原理太簡單?,留了一個彩蛋給你們,ABA問題是怎麼出現的,爲何不是AAB拖拉機,AAA金花,4個A炸彈 ?這一篇咱們再來揭開ABA的神祕面紗。git
面試的時候咱們也常常遭遇面試官的連環追問:程序員
案例:甲看見一個三角形積木,以爲很差看,想替換成五邊形,可是乙想把積木替換成四邊形。(前提條件,只能被替換一次)github
可能出現的過程如上圖所示:面試
三角形A
積木替換成五角星B1
五角星B1
替換成五邊形B2
五邊形B2
替換成棱形B3
棱形B3
替換成六邊形B4
六邊形B4
替換成三角形A
三角形V
替換成了五邊形B
**講解:**第一步道第五步,都是乙在替換,但最後仍是替換成了三角形(即時不是同一個三角形),這個就是ABA,A指最開始是三角形,B指中間被替換的B1/B2/B3/B4,第二個A就是第五步中的A,中間不論通過怎麼樣的形狀替換,最後仍是變成了三角形。而後甲再將A2和A1進行形狀比較,發現都是三角形,因此認爲乙沒有動過積木,甲能夠進行替換。這個就是比較並替換(CAS)中的ABA問題。編程
**小結:**CAS只管開頭和結尾,中間過程不關心,只要頭尾相同,則認爲能夠進行修改,而中間過程極可能被其餘人改過。小程序
AtomicReference
:原子引用類安全
/** 積木類 * @author: 悟空聊架構 * @create: 2020-08-25 */
class BuildingBlock {
String shape;
public BuildingBlock(String shape) {
this.shape = shape;
}
@Override
public String toString() {
return "BuildingBlock{" + "shape='" + shape + '}';
}
}
複製代碼
static BuildingBlock A = new BuildingBlock("三角形");
// 初始化一個積木對象B,形狀爲四邊形
static BuildingBlock B = new BuildingBlock("四邊形");
// 初始化一個積木對象D,形狀爲五邊形
static BuildingBlock D = new BuildingBlock("五邊形");
複製代碼
static AtomicReference<BuildingBlock> atomicReference = new AtomicReference<>(A);
複製代碼
new Thread(() -> {// 初始化一個積木對象A,形狀爲三角形
atomicReference.compareAndSet(A, B); // A->B
atomicReference.compareAndSet(B, A); // B->A
},
複製代碼
new Thread(() -> {// 初始化一個積木對象A,形狀爲三角形
try {
// 睡眠一秒,保證t1線程,完成了ABA操做
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 能夠替換成功,由於乙線程執行了A->B->A,形狀沒變,因此甲能夠進行替換。
System.out.println(atomicReference.compareAndSet(A, D) + "\t" + atomicReference.get()); // true BuildingBlock{shape='五邊形}
}, "甲").start();
複製代碼
**輸出結果:**true BuildingBlock{shape='五邊形}markdown
**小結:**當線程「乙」執行ABA以後,線程「甲」比較後,發現預期值和當前值一致,將三角形替換成了五邊形。架構
咱們看到乙無論怎麼進行操做,甲看到的仍是三角形,那甲當成乙沒有改變積木形狀 又有什麼問題呢?
出現的問題場景一般是帶有消耗類的場景,好比庫存減小,商品賣出。
(1)一家三口人,爸爸、媽媽、兒子。
(2)一天早上6點,媽媽給兒子的水杯灌滿了水(水量爲A),兒子先喝了一半(水量變成B)。
(3)而後媽媽把水杯又灌滿了(水量爲A),等中午再喝(媽媽執行了一個ABA操做)。
(4)爸爸7點看到水杯仍是滿的(不知道是媽媽又灌滿的),因而給兒子喝了1/3(水量變成D)
(5)那在中午以前,兒子喝了1/2+1/3= 5/6的水,這不是媽媽指望的,由於媽媽只想讓兒子中午以前喝半杯水。
這個場景的ABA問題帶來的後果就是原本只用喝1/2的水,結果喝了5/6的水。
(1)商品Y的庫存是10(A)
(2)用戶m購買了5件(B)
(3)運營人員乙補貨5件(A)(乙執行了一個ABA操做)
(4)運營人員甲看到庫存仍是10,就認爲一件也沒有賣出去(不考慮交易記錄),其實已經賣出去了5件。
那咱們怎麼解決原子引用的問題呢?
能夠用加版本號的方式來解決兩個A相同的問題,好比上面的積木案例,咱們能夠給兩個三角形都打上一個版本號的標籤,如A1和A2,在第六步中,形狀和版本號一致甲才能夠進行替換,因形狀都是三角形,而版本號一個1,一個是2,因此不能進行替換。
在Java代碼中,咱們能夠用原子時間戳引用類型:AtomicStampedReference
AtomicStampedReference
的底層代碼比較並替換方法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)));
}
複製代碼
expectedReference
:指望值
newReference
:替換值
expectedStamp
:指望版本號
newStamp
:替換版本號
先比較指望值expectedReference和當前值是否相等,以及指望版本號和當前版本號是否相等,若是二者都相等,則表示沒有被修改過,能夠進行替換。
(1)先定義3個積木:三角形A,四邊形B,五邊形D
// 初始化一個積木對象A,形狀爲三角形
BuildingBlock A = new BuildingBlock("三角形");
// 初始化一個積木對象B,形狀爲四邊形,乙會將三角形替換成四邊形
BuildingBlock B = new BuildingBlock("四邊形");
// 初始化一個積木對象B,形狀爲四邊形,乙會將三邊形替換成五邊形
BuildingBlock D = new BuildingBlock("五邊形");
複製代碼
(2)建立一個原子引用類型的實例 atomicReference
// 傳遞兩個值,一個是初始值,一個是初始版本號
AtomicStampedReference<BuildingBlock> atomicStampedReference = new AtomicStampedReference<>(A, 1);
複製代碼
(3)建立一個線程「乙」執行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,更新值B,指望版本號,更新版本號
atomicStampedReference.compareAndSet(A, B, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t 第二次版本號" + atomicStampedReference.getStamp()); //乙 第一次版本號1
// 2.比較並替換,傳入4個值,指望值B,更新值A,指望版本號,更新版本號
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,更新值B,指望版本號stamp,更新版本號stamp+1。A被替換爲B,當前版本號爲2
4)比較並替換,傳入4個值,指望值B,更新值A,指望版本號getStamp(),更新版本號getStamp()+1。B替換爲A,當前版本號爲3
(4)建立一個線程「甲」執行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 當前最新實際版本號:" + atomicStampedReference.getStamp()); // 甲 修改爲功否false 當前最新實際版本號:3
System.out.println(Thread.currentThread().getName() + "\t 當前實際最新值:" + atomicStampedReference.getReference()); // 甲 當前實際最新值:BuildingBlock{shape='三角形}
}, "甲").start();
複製代碼
(1)甲先獲取原子類的版本號,版本號爲1,由於乙線程還未執行ABA,因此甲獲取到的版本號和乙獲取到的版本號一致。
(2)暫停線程「甲」3秒鐘,使線程「乙」進行一次ABA替換操做
(3)乙執行完ABA操做後,線程甲執行比較替換,指望爲A,實際是A,版本號指望值是1,實際版本號是3
(4)雖然指望值和實際值都是A,可是版本號不一致,因此甲不能將A替換成D,這個就避免了ABA的問題。
小結: 帶版本號的原子引用類能夠利用CAS+版本號來比較變量是否被修改。
本篇分析了ABA產生的緣由,而後又列舉了生活中的兩個案例來分析ABA的危害。而後提出了怎麼解決ABA問題:用帶版本號的原子引用類AtomicStampedReference。
限於篇幅和側重點,CAS的優化並無涉及到,後續再倒騰這一塊吧。另外AtomicStampedReference的缺點本篇本沒有進行講解,限於筆者的技術水平緣由,並無一一做答,期待後續能補上這一塊的解答。
我是悟空,一隻努力變強的碼農!我要變身超級賽亞人啦!
另外能夠搜索「悟空聊架構」或者PassJava666,一塊兒進步! 個人GitHub主頁,關注個人
Spring Cloud
實戰項目《佳必過》
你好,我是
悟空哥
,「7年項目開發經驗,全棧工程師,開發組長,超喜歡圖解編程底層原理」。
我還手寫了 2 個小程序
,Java 刷題小程序
,PMP 刷題小程序
,點擊個人公衆號菜單打開!
另外有 111 本架構師資料以及 1000 道 Java 面試題,都整理成了PDF。
能夠關注公衆號 「悟空聊架構」 回覆 悟空
領取優質資料。
「轉發->在看->點贊->收藏->評論!!!」 是對我最大的支持!
《Java併發必知必會》系列:
1.反制面試官 | 14張原理圖 | 不再怕被問 volatile!