獨占鎖:是一種悲觀鎖,synchronized就是一種獨占鎖,會導致其它所有需要鎖的線程掛起,等待持有鎖的線程釋放鎖。
樂觀鎖:每次不加鎖,假設沒有沖突去完成某項操作,如果因為沖突失敗就重試,直到成功為止。
一、CAS 操作
樂觀鎖用到的機制就是CAS,Compare and Swap。
CAS有3個操作數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改為B,否則什么都不做。
1、非阻塞算法 (nonblocking algorithms)
一個線程的失敗或者掛起不應該影響其他線程的失敗或掛起的算法。
現代的CPU提供了特殊的指令,可以自動更新共享數據,而且能夠檢測到其他線程的干擾,而 compareAndSet() 就用這些代替了鎖定。
2、AtomicInteger示例
拿出AtomicInteger來研究在沒有鎖的情況下是如何做到數據正確性的。
private volatile int value;
在沒有鎖的機制下需要借助volatile原語,保證線程間的數據是可見的(共享的)。
這樣才獲取變量的值的時候才能直接讀取。
public final int get() { return value; }
然后來看看 ++i 是怎么做到的。
public final int incrementAndGet() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return next; } }
在這里采用了CAS操作,每次從內存中讀取數據然后將此數據和+1后的結果進行CAS操作,如果成功就返回結果,否則重試直到成功為止。
而compareAndSet利用JNI來完成CPU指令的操作。
public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
整體的過程就是這樣子的,利用CPU的CAS指令,同時借助JNI來完成Java的非阻塞算法。其它原子操作都是利用類似的特性完成的。
而整個J.U.C都是建立在CAS之上的,因此對於synchronized阻塞算法,J.U.C在性能上有了很大的提升。參考資料的文章中介紹了如果利用CAS構建非阻塞計數器、隊列等數據結構。
二、ABA問題
CAS看起來很爽,但是會導致“ABA問題”。
CAS算法實現一個重要前提需要取出內存中某時刻的數據,而在下時刻比較並替換,那么在這個時間差類會導致數據的變化。
比如說一個線程one從內存位置V中取出A,這時候另一個線程two也從內存中取出A,並且two進行了一些操作變成了B,然后two又將V位置的數據變成A,這時候線程one進行CAS操作發現內存中仍然是A,然后one操作成功。盡管線程one的CAS操作成功,但是不代表這個過程就是沒有問題的。如果鏈表的頭在變化了兩次后恢復了原值,但是不代表鏈表就沒有變化。因此前面提到的原子操作AtomicStampedReference/AtomicMarkableReference就很有用了。這允許一對變化的元素進行原子操作。
在運用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>也實現了這個作用,它通過包裝[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 = new AtomicStampedReference<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(); } }
Reference:
深入淺出 Java Concurrency (5): 原子操作 part 4
用AtomicStampedReference解決ABA問題