狸貓換太子
#在開始問題的闡述之前,我們先來看看一則小故事:
北宋宋真宗皇后死後,當時他的兩位愛妃劉妃和李妃都懷了孕,很顯然,誰生了兒子,誰就有可能立為正宮。劉妃久懷嫉妒之心,唯恐李妃生了兒子被立為皇后,於是與宮中總管都堂郭槐定計,在接生婆尤氏的配合下,乘李妃分娩時由於血暈而人事不知之機,將一狸貓剝去皮毛,血淋淋,光油油地換走了剛出世的太子。劉妃命宮女寇珠勒死太子,寇珠於心不忍,暗中將太子交付宦官陳琳,陳琳將太子裝在提盒中送至八賢王處撫養。再說真宗看到被剝了皮的狸貓,以為李妃產下了一個妖物,乃將其貶入冷宮。不久,劉妃臨產,生了個兒子,被立為太子,劉妃也被冊立為皇后。誰知六年後,劉後之子病夭。真宗再無子嗣,就將其皇兄八賢王之子(實為當年被換走的皇子)收為義子,並立為太子。
從這故事看出來,太子出生被換成了狸貓,最後陰差陽錯又回歸成為太子。雖然結果是一樣的,但是過程曲折了,太子真的是命途多舛啊。
為什麼要說這個故事?其實跟我們今天要介紹的問題有很大的關係。同樣的結果,可能中間不知道發生了多少次操作,那我們能認為他沒改變嗎?在不同的業務場景下,我們要仔細考慮這個問題。
ABA問題描述
#在多執行緒場景下CAS
會出現 ABA
問題,關於ABA問題這裡簡單科普下,例如有2個線程同時對同一個值(初始值為A)進行CAS操作,這三個線程如下:
執行緒1,期望值為A,欲更新的值為B #執行緒2,期望值為A,欲更新的值為B
線程1搶先獲得CPU時間片,而線程2因為其他原因阻塞了,線程1取值與期望的A值比較,發現相等然後將值更新為B,然後這個時候出現了線程3,期望值為B,欲更新的值為A,線程3取值與期望的值B比較,發現相等則將值更新為A,此時線程2從阻塞中恢復,並且獲得了CPU時間片,這時候線程2取值與期望的值A比較,發現相等則將值更新為B,雖然線程2也完成了操作,但是線程2並不知道值已經經過了A->B->A的變化過程。
舉個具體的例子說明
#小明在提款機,提取了50元,因為提款機問題,有兩個線程,同時把餘額從100變成50:
-
#線程1(提款機):取得目前值100,期望更新為50; 執行緒2(提款機):取得目前值100,期望更新為50; 執行緒1成功執行,執行緒2某種原因block了; 這時,某人給小明匯款50; 線程3(預設):取得目前值50,期望更新為100,這時候線程3成功執行,餘額變成100; #線程2從Block恢復,取得到的也是100,compare之後,繼續更新餘額為50。
此時可以看到,實際餘額應該是100(100-50 50)
,但實際上變成了50(100-50 50 -50)
這就是ABA問題帶來錯誤提交結果。
解決方法
#要解決ABA問題,可以增加一個版本號,當記憶體位置V的值每次被修改後,版本號碼都加1
程式碼範例
#透過AtomicStampedReference來解決ABA問題
AtomicStampedReference內部維護了物件值和版本號,在建立AtomicStampedReference物件時,需要傳入初始值和初始版本號;
#當AtomicStampedReference設定物件值時,物件值以及狀態戳記都必須滿足期望值,寫入才會成功。
3、執行緒t1完成ABA操作,版本號遞增到3
private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(100,1); public static void main(String[] args) { //第一个线程 new Thread(() -> { System.out.println("t1拿到的初始版本号:" + atomicStampedReference.getStamp()); //睡眠1秒,是为了让t2线程也拿到同样的初始版本号 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } atomicStampedReference.compareAndSet(100, 101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1); atomicStampedReference.compareAndSet(101, 100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1); },"t1").start(); // 第二个线程 new Thread(() -> { int stamp = atomicStampedReference.getStamp(); System.out.println("t2拿到的初始版本号:" + stamp); //睡眠3秒,是为了让t1线程完成ABA操作 try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("最新版本号:" + atomicStampedReference.getStamp()); System.out.println(atomicStampedReference.compareAndSet(100, 2019,stamp,atomicStampedReference.getStamp() + 1) + "\t当前值:" + atomicStampedReference.getReference()); },"t2").start(); }
1、初始值100,初始版本號1 2、執行緒t1和t2拿到一樣的初始版本號
4、執行緒t2完成CAS操作,最新版本號碼已經變成3,跟線程t2之前拿到的版本號1不相等,操作失敗
###執行結果:######t1拿到的初始版本号:1 t2拿到的初始版本号:1 最新版本号:3 false 当前值:100################透過AtomicMarkableReference解決ABA問題############# ##AtomicStampedReference###可以為引用加上版本號,追蹤引用的整個變化過程,如:A -> B -> C -> D -> A,透過AtomicStampedReference,我們可以知道,引用變量中途被更改了3次。但是,有時候,我們並不關心引用變數更改了幾次,只是單純的關心是否更改過,所以就有了###AtomicMarkableReference###, AtomicMarkableReference的唯一差異就是不再用int識別來引用,而是使用boolean變數-表示引用變數是否被更改過。 ###
private static AtomicMarkableReference<Integer> atomicMarkableReference = new AtomicMarkableReference<Integer>(100,false); public static void main(String[] args) { // 第一个线程 new Thread(() -> { System.out.println("t1版本号是否被更改:" + atomicMarkableReference.isMarked()); //睡眠1秒,是为了让t2线程也拿到同样的初始版本号 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } atomicMarkableReference.compareAndSet(100, 101,atomicMarkableReference.isMarked(),true); atomicMarkableReference.compareAndSet(101, 100,atomicMarkableReference.isMarked(),true); },"t1").start(); // 第二个线程 new Thread(() -> { boolean isMarked = atomicMarkableReference.isMarked(); System.out.println("t2版本号是否被更改:" + isMarked); //睡眠3秒,是为了让t1线程完成ABA操作 try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("是否更改过:" + atomicMarkableReference.isMarked()); System.out.println(atomicMarkableReference.compareAndSet(100, 2019,isMarked,true) + "\t当前值:" + atomicMarkableReference.getReference()); },"t2").start(); }######1、初始值100,初始版本號未修改false ###2、線程t1和t2拿到一樣的初始版本號都沒有被修改false ###3、線程t1完成ABA操作,版本號碼被修改true ###4、線程t2完成CAS操作,版本號碼已經變成true,跟線程t2之前拿到的版本號false不相等,操作失敗####### #####執行結果:######
t1版本号是否被更改:false t2版本号是否被更改:false 是否更改过:true false 当前值:100
多说几句
以上是本期关于CAS领域的一个经典ABA问题的解析,不知道你在实际的工作中有没有遇到过,但是在面试中这块是并发知识考查的重点。如果你还没接触过此类的问题,我的建议是你自己将上面的代码运行一下,结合理论去理解一下ABA问题所带来的问题以及如何解决他,这对你日后的开发工作也是有莫大的帮助的!
以上是面試官問你:你知道什麼是ABA問題嗎?的詳細內容。更多資訊請關注PHP中文網其他相關文章!

熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

AI Hentai Generator
免費產生 AI 無盡。

熱門文章

熱工具

禪工作室 13.0.1
強大的PHP整合開發環境

WebStorm Mac版
好用的JavaScript開發工具

MantisBT
Mantis是一個易於部署的基於Web的缺陷追蹤工具,用於幫助產品缺陷追蹤。它需要PHP、MySQL和一個Web伺服器。請查看我們的演示和託管服務。

SublimeText3 Linux新版
SublimeText3 Linux最新版

記事本++7.3.1
好用且免費的程式碼編輯器