傳說中的並發編程ABA問題
什麽是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失敗:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABA {
private static AtomicInteger atomicInt = new AtomicInteger(100);
private static AtomicStampedReference atomicStampedRef = new AtomicStampedReference(100, 0);
public static void main(String[] args) throws InterruptedException {
Thread intT1 = new Thread(new Runnable() {
@Override
public void run() {
atomicInt.compareAndSet(100, 101);
atomicInt.compareAndSet(101, 100);
}
});
Thread intT2 = new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
boolean c3 = atomicInt.compareAndSet(100, 101);
System.out.println(c3); // true
}
});
intT1.start();
intT2.start();
intT1.join();
intT2.join();
Thread refT1 = new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
atomicStampedRef.compareAndSet(100, 101, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
atomicStampedRef.compareAndSet(101, 100, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
}
});
Thread refT2 = new Thread(new Runnable() {
@Override
public void run() {
int stamp = atomicStampedRef.getStamp();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
}
boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1);
System.out.println(c3); // false
}
});
refT1.start();
refT2.start();
}
}
總結
簡單說來,在多線程環境中,為了進行性能上的優化,可以設計一些無鎖的算法。這裏面會需要進行較多的判斷,有些判斷是十分關鍵的(比如說CAS中的判斷),ABA主要存在這些判斷中。有的時候,我們並不只是需要判斷變量是不是我們看到的那個值,還需要在執行操作的過程中,判斷這個變量是否已經發生了改變。得益於垃圾回收機制,用Java設計無鎖算法的時候,可能出現ABA問題的情況還是相對比較少的。
傳說中的並發編程ABA問題