Java CAS ABA問題發生的場景分析
提到了CAS操作存在問題,就是在CAS之前A變成B又變回A,CAS還是能夠設置成功的,什麽場景下會出現這個問題呢?查了一些資料,發現在下面的兩種情況下會出現ABA問題。
1.A最開始的內存地址是X,然後失效了,有分配了B,恰好內存地址是X,這時候通過CAS操作,卻設置成功了
這種情況在帶有GC的語言中,這種情況是不可能發生的,為什麽呢?拿JAVA舉例,在執行CAS操作時,A,B對象肯定生命周期內,GC不可能將其釋放,那麽A指向的內存是不會被釋放的,B也就不可能分配到與A相同的內存地址,CAS失敗。若在無GC的,A對象已經被釋放了,那麽B被分配了A的內存,CAS成功。
2.線程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>也實現了這個作用,它通過包裝[E,Integer]的元組來對對象標記版本戳stamp,從而避免ABA問題,例如下面的代碼分別用AtomicInteger和AtomicStampedReference來對初始值為100的原子整型變量進行更新,AtomicInteger會成功執行CAS操作,而加上版本戳的AtomicStampedReference對於ABA問題會執行CAS失敗
package concur.lock; 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<Integer> atomicStampedRef = newAtomicStampedReference<Integer>(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) { e.printStackTrace(); } 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) { e.printStackTrace(); } 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(); System.out.println("before sleep : stamp = " + stamp); // stamp = 0 try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("after sleep : stamp = " + atomicStampedRef.getStamp());//stamp = 1 boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp+1); System.out.println(c3); //false } }); refT1.start(); refT2.start(); } }
Java CAS ABA問題發生的場景分析