Java中CAS詳解(悲觀鎖與樂觀鎖)
前言:在JDK1.5之前Java語言是靠synchronized關鍵字保證同步的,這會導致有鎖鎖機制存在以下問題:
(1)在多執行緒競爭下,加鎖、釋放鎖會導致比較多的上下文切換和排程延時,引起效能問題。
(2)一個執行緒持有鎖會導致其它所有需要此鎖的執行緒掛起。
(3)如果一個優先順序高的執行緒等待一個優先順序低的執行緒釋放鎖會導致優先順序倒置,引起效能風險。
volatile是不錯的機制,但是volatile不能保證原子性,因此對於同步最終還是要回到鎖機制上來。
獨佔鎖是一種悲觀鎖,synchronized就是一種獨佔鎖,會導致其它所有需要鎖的執行緒掛起,等待持有鎖的執行緒釋放鎖
而另一個更加有效的鎖就是樂觀鎖。所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操作,如果因為衝突失敗就重試,直到成功為止。樂觀鎖用到的機制就是CAS,Compare and Swap(比較與交換)。
一、什麼是CAS
CAS,Compare and Swap(比較與交換)。
我們都知道,在java語言之前,併發就已經廣泛存在並在伺服器領域得到了大量的應用。所以硬體廠商老早就在晶片中加入了大量直至併發操作的原語,從而在硬體層面提升效率。在intel的CPU中,使用cmpxchg指令。
在Java發展初期,java語言是不能夠利用硬體提供的這些便利來提升系統的效能的。而隨著java不斷的發展,Java本地方法(JNI)的出現,使得java程式越過JVM直接呼叫本地方法提供了一種便捷的方式,因而java在併發的手段上也多了起來。而在Doug Lea提供的cucurenct包中,CAS理論是它實現整個java包的基石。
CAS 操作包含三個運算元 —— 記憶體位置(V)、預期原值(A)和新值(B)。 如果記憶體位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新為新值 。否則,處理器不做任何操作。無論哪種情況,它都會在 CAS 指令之前返回該 位置的值。(在 CAS 的一些特殊情況下將僅返回 CAS 是否成功,而不提取當前值)CAS 有效地說明了“我認為位置 V 應該包含值 A;如果包含該值,則將 B 放到這個位置;否則,不要更改該位置,只告訴我這個位置現在的值即可。”
通常將 CAS 用於同步的方式是從地址 V 讀取值 A,執行多步計算來獲得新 值 B,然後使用 CAS 將 V 的值從 A 改為 B。如果 V 處的值尚未同時更改,則 CAS 操作成功。
類似於 CAS 的指令允許演算法執行讀-修改-寫操作,而無需害怕其他執行緒同時 修改變數,因為如果其他執行緒修改變數,那麼 CAS 會檢測它(並失敗),演算法 可以對該操作重新計算。
二、CAS的目的
利用CPU的CAS指令,同時藉助JNI來完成Java的非阻塞演算法。其它原子操作都是利用類似的特性完成的。而整個J.U.C都是建立在CAS之上的,因此對於synchronized阻塞演算法,J.U.C在效能上有了很大的提升。
三、CAS存在的問題
CAS雖然很高效的解決原子操作,但是CAS仍然存在以下三大問題:
1、ABA問題。因為CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號,在變數前面追加上版本號,每次變數更新的時候把版本號加一,那麼A-B-A 就會變成1A - 2B-3A。
從Java1.5開始JDK的atomic包裡提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法作用是首先檢查當前引用是否等於預期引用,並且當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設定為給定的更新值。
舉個例子:
update table set x=x+1, version=version+1 where id=#{id} and version=#{version};
2、迴圈時間長開銷大。自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷。如果JVM能支援處理器提供的pause指令那麼效率會有一定的提升,pause指令有兩個作用,第一它可以延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零。第二它可以避免在退出迴圈的時候因記憶體順序衝突(memory order violation)而引起CPU流水線被清空(CPU pipeline flush),從而提高CPU的執行效率。
3、只能保證一個共享變數的原子操作。當對一個共享變數執行操作時,我們可以使用迴圈CAS的方式來保證原子操作,但是對多個共享變數操作時,迴圈CAS就無法保證操作的原子性,這個時候就可以用鎖,或者有一個取巧的辦法,就是把多個共享變數合併成一個共享變數來操作。比如有兩個共享變數i=2,j=a,合併一下ij=2a,然後用CAS來操作ij。
從Java1.5開始JDK提供了AtomicReference類來保證引用物件之間的原子性,你可以把多個變數放在一個物件裡來進行CAS操作。
四、concurrent包的實現
由於java的CAS同時具有volatile讀和volatile寫的記憶體語義,因此Java執行緒之間的通訊現在有了下面四種方式:
(1)A執行緒寫volatile變數,隨後B執行緒讀這個volatile變數。
(2)A執行緒寫volatile變數,隨後B執行緒用CAS更新這個volatile變數。
(3)A執行緒用CAS更新一個volatile變數,隨後B執行緒用CAS更新這個volatile變數。
(4)A執行緒用CAS更新一個volatile變數,隨後B執行緒讀這個volatile變數。
Java的CAS會使用現代處理器上提供的高效機器級別原子指令,這些原子指令以原子方式對記憶體執行讀-改-寫操作,這是在多處理器中實現同步的關鍵(從本質上來說,能夠支援原子性讀-改-寫指令的計算機器,是順序計算圖靈機的非同步等價機器,因此任何現代的多處理器都會去支援某種能對記憶體執行原子性讀-改-寫操作的原子指令)。同時,volatile變數的讀/寫和CAS可以實現執行緒之間的通訊。把這些特性整合在一起,就形成了整個concurrent包得以實現的基石。如果我們仔細分析concurrent包的原始碼實現,會發現一個通用化的實現模式:
首先,宣告共享變數為volatile;
然後,使用CAS的原子條件更新來實現執行緒之間的同步;
同時,配合以volatile的讀/寫和CAS所具有的volatile讀和寫的記憶體語義來實現執行緒之間的通訊。
AQS,非阻塞資料結構和原子變數類(java.util.concurrent.atomic包中的類),這些concurrent包中的基礎類都是使用這種模式來實現的,而concurrent包中的高層類又是依賴於這些基礎類來實現的,從整體來看,concurrent包的實現示意圖如下: