CAS (全 ) && concurrent包的實現
在JDK 5之前Java語言是靠synchronized關鍵字保證同步的,這會導致有鎖。
鎖機制存在以下問題:
(1)在多執行緒競爭下,加鎖、釋放鎖會導致比較多的上下文切換和排程延時,引起效能問題。
(2)一個執行緒持有鎖會導致其它所有需要此鎖的執行緒掛起。
(3)如果一個優先順序高的執行緒等待一個優先順序低的執行緒釋放鎖會導致優先順序倒置,引起效能風險。
volatile是不錯的機制,但是volatile不能保證原子性。因此對於同步最終還是要回到鎖機制上來。
CAS(樂觀鎖的實現):
當多個執行緒使用CAS獲取鎖,只能有一個成功,其他執行緒返回失敗,繼續嘗試獲取鎖;
CAS操作中包含三個引數:V(需讀寫的記憶體位置的值)+A(準備用來比較的引數)+B(準備寫入的新值):若A的引數與V值相匹配,就寫入B;若不匹配,就不進行操作。
JAVA中,sun.misc.Unsafe 類提供了硬體級別的原子操作來實現這個CAS。 java.util.concurrent 包下的大量類都使用了這個 Unsafe.java 類的CAS操作(下文我們會講到concurrent包的具體實現)。
我們先拿AtomicInteger來研究在沒有鎖的情況下是如何做到資料正確性的。
private volatile int value; 首先,在沒有鎖的機制下可能需要藉助volatile原語,保證執行緒間的資料是可見的(共享的)。這樣才獲取變數的值的時候才能直接讀取。 public final int get() { return value; } public final int incrementAndGet() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return next; } }
compareAndSet利用JNI來完成CPU指令的操作。
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
java.util.concurrent.atomic包中對CAS的實現是通過synchronized關鍵字實現的:
public final synchronized boolean compareAndSet(long expect, long update) { if (value == expect) { value = update; return true; } else { return false; } }
整體的過程就是:利用CPU的CAS指令,同時藉助JNI來完成Java的非阻塞演算法。
CAS會導致ABA的問題:
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丟掉了。
自旋CAS的基本思路就是迴圈進行CAS操作,直到成功為止。
CAS雖然很高效的解決原子操作,但是CAS仍然存在三大問題:
ABA問題 迴圈時間長開銷大 只能保證一個共享變數的原子操作
(1)ABA問題。如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時發現它的值沒有發生變化,但實際上卻發生了。ABA的解決思路就是使用版本號,在變數前面追加版本號,那麼A——B——A 就變成了1A——2B——3A。
從Java1.5開始JDK的atomic包裡提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法作用是首先檢查當前引用是否等於預期引用,並且當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設定為給定的更新值。
(2)迴圈時間長開銷大 :自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷。如果JVM能支援處理器提供的pause指令那麼效率會有一定的提升,pause指令有兩個作用,第一它可以延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零。第二它可以避免在退出迴圈的時候因記憶體順序衝突(memory order violation)而引起CPU流水線被清空(CPU pipeline flush),從而提高CPU的執行效率。
(3)只能保證一個共性變數的原子操作。也就是多個共享變數操作時,迴圈CAS就無法保證操作的原子性了。但從JDK1.5開始,JDK提供了AtomicReference類來保證引用物件之間的原子性,就可以把多個變數放在一個物件裡來進行CAS操作。
concurrent包的實現
由於java的CAS同時具有 volatile 讀和volatile寫的記憶體語義,因此Java執行緒之間的通訊現在有了下面四種方式:
- A執行緒寫volatile變數,隨後B執行緒讀這個volatile變數。
A執行緒寫volatile變數,隨後B執行緒用CAS更新這個volatile變數。
A執行緒用CAS更新一個volatile變數,隨後B執行緒用CAS更新這個volatile變數。
A執行緒用CAS更新一個volatile變數,隨後B執行緒讀這個volatile變數。
Java的CAS使用的是現代處理器上提供的高效機器級別原子指令,這些原子指令以原子的方式對記憶體進行讀改寫的操作,這是在多處理器下實現同步的關鍵。
同時,volatile關鍵字修飾的變數的讀寫和CAS之間可以實現執行緒之間的通訊。
以上兩種特性整合在一起,就形成了整個concurrent包得以實現的基石。我們通過concurrent包的原始碼實現可以發現一個模式:
1.首先宣告共享變數volatile;
2.然後使用CAS的原子條件更新來實現執行緒之間的同步;
3.同時,配合以volatile的讀寫和CAS所具有的volatile讀和寫的記憶體語義來實現執行緒之間的通訊。
AQS 非阻塞資料結構和原子變數類(Atomic類)都是java.util.concurrent.atomic包中的類,這些基礎類都是使用上述模式實現的,concurrent包中的高層類又是依賴於這些基礎類來實現的。
總結
可以用CAS在無鎖的情況下實現原子操作,但要明確應用場合,非常簡單的操作且又不想引入鎖可以考慮使用CAS操作,當想要非阻塞地完成某一操作也可以考慮CAS。不推薦在複雜操作中引入CAS,會使程式可讀性變差,且難以測試,同時會出現ABA問題。
之後的文章中我們將詳細講解Atomic相關知識以及AQS(AbstractQueuedSynchronizer)的用法。