首頁  >  文章  >  Java  >  面試官問你:你知道什麼是ABA問題嗎?

面試官問你:你知道什麼是ABA問題嗎?

Java学习指南
Java学习指南轉載
2023-07-26 15:09:451222瀏覽

狸貓換太子

#在開始問題的闡述之前,我們先來看看一則小故事:

北宋宋真宗皇后死後,當時他的兩位愛妃劉妃和李妃都懷了孕,很顯然,誰生了兒子,誰就有可能立為正宮。劉妃久懷嫉妒之心,唯恐李妃生了兒子被立為皇后,於是與宮中總管都堂郭槐定計,在接生婆尤氏的配合下,乘李妃分娩時由於血暈而人事不知之機,將一狸貓剝去皮毛,血淋淋,光油油地換走了剛出世的太子。劉妃命宮女寇珠勒死太子,寇珠於心不忍,暗中將太子交付宦官陳琳,陳琳將太子裝在提盒中送至八賢王處撫養。再說真宗看到被剝了皮的狸貓,以為李妃產下了一個妖物,乃將其貶入冷宮。不久,劉妃臨產,生了個兒子,被立為太子,劉妃也被冊立為皇后。誰知六年後,劉後之子病夭。真宗再無子嗣,就將其皇兄八賢王之子(實為當年被換走的皇子)收為義子,並立為太子。

從這故事看出來,太子出生被換成了狸貓,最後陰差陽錯又回歸成為太子。雖然結果是一樣的,但是過程曲折了,太子真的是命途多舛啊。

為什麼要說這個故事?其實跟我們今天要介紹的問題有很大的關係。同樣的結果,可能中間不知道發生了多少次操作,那我們能認為他沒改變嗎?在不同的業務場景下,我們要仔細考慮這個問題。

ABA問題描述

#在多執行緒場景下CAS會出現 ABA問題,關於ABA問題這裡簡單科普下,例如有2個線程同時對同一個值(初始值為A)進行CAS操作,這三個線程如下:

  1. 執行緒1,期望值為A,欲更新的值為B
  2. #執行緒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設定物件值時,物件值以及狀態戳記都必須滿足期望值,寫入才會成功。


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拿到一樣的初始版本號
3、執行緒t1完成ABA操作,版本號遞增到3

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中文網其他相關文章!

陳述:
本文轉載於:Java学习指南。如有侵權,請聯絡admin@php.cn刪除