1. 程式人生 > >傳說中的並發編程ABA問題

傳說中的並發編程ABA問題

ren set 情況 out 離開 描述 分享 boolean int

什麽是ABA問題

ABA並不是一個縮寫,更像是一個形象的描述。ABA問題出現在多線程或多進程計算環境中。

首先描述ABA。假設兩個線程T1和T2訪問同一個變量V,當T1訪問變量V時,讀取到V的值為A;此時線程T1被搶占了,T2開始執行,T2先將變量V的值從A變成了B,然後又將變量V從B變回了A;此時T1又搶占了主動權,繼續執行,它發現變量V的值還是A,以為沒有發生變化,所以就繼續執行了。這個過程中,變量V從A變為B,再由B變為A就被形象地稱為ABA問題了。

上面的描述看上去並不會導致什麽問題。T1中的判斷V的值是A就不應該有問題的,無論是開始的A,還是ABA後面的A,判斷的結果應該是一樣的才對。

不容易看出問題的主要還是因為:“值是一樣的”等同於“沒有發生變化”(就算被改回去了,那也是變化)的認知。畢竟在大多數程序代碼中,我們只需要知道值是不是一樣的,並不關心它在之前的過程中有沒有發生變化;所以,當我需要知道之前的過程中“有沒有發生變化”的時候,ABA就是問題了。

現實ABA問題

警匪劇看多了人應該可以快速反應到發生了什麽。應用到ABA問題,首先,這裏的A和B並不表示被掉的包這個實物,而是掉包過程中的狀態的變化。假設一個裝有10000W箱子(別管它有多大)放在一個房間裏,10分鐘後再進去拿出來贖人去。但是,有個賊在這10分鐘內進去(別管他是怎麽進去的)用一個同樣大小的空箱子,把我的箱子掉包了。當我再進去看的時候,發現箱子還在,自然也就以為沒有問題了的,就繼續拿著桌子上的箱子去贖人了(別管重量對不對)。現在只要知道這裏有問題就行了,拿著沒錢的箱子去贖人還沒有問題麽?

這裏的變量V就是桌子上是否有箱子的狀態。A,是桌子上有箱子的狀態;B是箱子在掉包過程中,離開桌子,桌子上沒有箱子的狀態;最後一個A也是桌子上有箱子的狀態。但是箱子裏面的東西是什麽就不不知道了。

程序世界的ABA問題

在運用CAS做Lock-Free操作中有一個經典的ABA問題:

線程1準備用CAS將變量的值由A替換為B,在此之前,線程2將變量的值由A替換為C,又由C替換為A,然後線程1執行CAS時發現變量的值仍然為A,所以CAS成功。但實際上這時的現場已經和最初不同了,盡管CAS成功,但可能存在潛藏的問題,例如下面的例子:
技術分享

現有一個用單向鏈表實現的堆棧,棧頂為A,這時線程T1已經知道A.next為B,然後希望用CAS將棧頂替換為B:

head.compareAndSet(A,B);

在T1執行上面這條指令之前,線程T2介入,將A、B出棧,再pushD、C、A,此時堆棧結構如下圖,而對象B此時處於遊離狀態:

技術分享

此時輪到線程T1執行CAS操作,檢測發現棧頂仍為A,所以CAS成功,棧頂變為B,但實際上B.next為null,所以此時的情況變為:

技術分享

其中堆棧中只有B一個元素,C和D組成的鏈表不再存在於堆棧中,平白無故就把C、D丟掉了。

以上就是由於ABA問題帶來的隱患,各種樂觀鎖的實現中通常都會用版本戳version來對記錄或對象標記,避免並發操作帶來的問題,在Java中,AtomicStampedReference也實現了這個作用,它通過包裝[E,Integer]的元組來對對象標記版本戳stamp,從而避免ABA問題,例如下面的代碼分別用AtomicInteger和AtomicStampedReference來對初始值為100的原子整型變量進行更新,AtomicInteger會成功執行CAS操作,而加上版本戳的AtomicStampedReference對於ABA問題會執行CAS失敗:

  1. import java.util.concurrent.TimeUnit;
  2. import java.util.concurrent.atomic.AtomicInteger;
  3. import java.util.concurrent.atomic.AtomicStampedReference;
  4. public class ABA {
  5. private static AtomicInteger atomicInt = new AtomicInteger(100);
  6. private static AtomicStampedReference atomicStampedRef = new AtomicStampedReference(100, 0);
  7. public static void main(String[] args) throws InterruptedException {
  8. Thread intT1 = new Thread(new Runnable() {
  9. @Override
  10. public void run() {
  11. atomicInt.compareAndSet(100, 101);
  12. atomicInt.compareAndSet(101, 100);
  13. }
  14. });
  15. Thread intT2 = new Thread(new Runnable() {
  16. @Override
  17. public void run() {
  18. try {
  19. TimeUnit.SECONDS.sleep(1);
  20. } catch (InterruptedException e) {
  21. }
  22. boolean c3 = atomicInt.compareAndSet(100, 101);
  23. System.out.println(c3); // true
  24. }
  25. });
  26. intT1.start();
  27. intT2.start();
  28. intT1.join();
  29. intT2.join();
  30. Thread refT1 = new Thread(new Runnable() {
  31. @Override
  32. public void run() {
  33. try {
  34. TimeUnit.SECONDS.sleep(1);
  35. } catch (InterruptedException e) {
  36. }
  37. atomicStampedRef.compareAndSet(100, 101, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
  38. atomicStampedRef.compareAndSet(101, 100, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
  39. }
  40. });
  41. Thread refT2 = new Thread(new Runnable() {
  42. @Override
  43. public void run() {
  44. int stamp = atomicStampedRef.getStamp();
  45. try {
  46. TimeUnit.SECONDS.sleep(2);
  47. } catch (InterruptedException e) {
  48. }
  49. boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1);
  50. System.out.println(c3); // false
  51. }
  52. });
  53. refT1.start();
  54. refT2.start();
  55. }
  56. }

總結

簡單說來,在多線程環境中,為了進行性能上的優化,可以設計一些無鎖的算法。這裏面會需要進行較多的判斷,有些判斷是十分關鍵的(比如說CAS中的判斷),ABA主要存在這些判斷中。有的時候,我們並不只是需要判斷變量是不是我們看到的那個值,還需要在執行操作的過程中,判斷這個變量是否已經發生了改變。得益於垃圾回收機制,用Java設計無鎖算法的時候,可能出現ABA問題的情況還是相對比較少的。

傳說中的並發編程ABA問題