#在開始問題的闡述之前,我們先來看看一則小故事:
北宋宋真宗皇后死後,當時他的兩位愛妃劉妃和李妃都懷了孕,很顯然,誰生了兒子,誰就有可能立為正宮。劉妃久懷嫉妒之心,唯恐李妃生了兒子被立為皇后,於是與宮中總管都堂郭槐定計,在接生婆尤氏的配合下,乘李妃分娩時由於血暈而人事不知之機,將一狸貓剝去皮毛,血淋淋,光油油地換走了剛出世的太子。劉妃命宮女寇珠勒死太子,寇珠於心不忍,暗中將太子交付宦官陳琳,陳琳將太子裝在提盒中送至八賢王處撫養。再說真宗看到被剝了皮的狸貓,以為李妃產下了一個妖物,乃將其貶入冷宮。不久,劉妃臨產,生了個兒子,被立為太子,劉妃也被冊立為皇后。誰知六年後,劉後之子病夭。真宗再無子嗣,就將其皇兄八賢王之子(實為當年被換走的皇子)收為義子,並立為太子。
從這故事看出來,太子出生被換成了狸貓,最後陰差陽錯又回歸成為太子。雖然結果是一樣的,但是過程曲折了,太子真的是命途多舛啊。
為什麼要說這個故事?其實跟我們今天要介紹的問題有很大的關係。同樣的結果,可能中間不知道發生了多少次操作,那我們能認為他沒改變嗎?在不同的業務場景下,我們要仔細考慮這個問題。
#在多執行緒場景下CAS
會出現 ABA
問題,關於ABA問題這裡簡單科普下,例如有2個線程同時對同一個值(初始值為A)進行CAS操作,這三個線程如下:
線程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:
此時可以看到,實際餘額應該是100(100-50 50)
,但實際上變成了50(100-50 50 -50)
這就是ABA問題帶來錯誤提交結果。
#要解決ABA問題,可以增加一個版本號,當記憶體位置V的值每次被修改後,版本號碼都加1
#當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中文網其他相關文章!