樂觀鎖的一種實現方式——CAS
原文出處: hollischuang (@Hollis_Chuang)
在深入理解樂觀鎖與悲觀鎖一文中我們介紹過鎖。本文在這篇文章的基礎上,深入分析一下樂觀鎖的實現機制,介紹什麽是CAS、CAS的應用以及CAS存在的問題等。
線程安全
眾所周知,Java是多線程的。但是,Java對多線程的支持其實是一把雙刃劍。一旦涉及到多個線程操作共享資源的情況時,處理不好就可能產生線程安全問題。線程安全性可能是非常復雜的,在沒有充足的同步的情況下,多個線程中的操作執行順序是不可預測的。
Java裏面進行多線程通信的主要方式就是共享內存的方式,共享內存主要的關註點有兩個:可見性和有序性。加上復合操作的原子性,我們可以認為Java的線程安全性問題主要關註點有3個:可見性、有序性和原子性。
Java內存模型(JMM)解決了可見性和有序性的問題,而鎖解決了原子性的問題。這裏不再詳細介紹JMM及鎖的其他相關知識。但是我們要討論一個問題,那就是鎖到底是不是有利無弊的?
鎖存在的問題
Java在JDK1.5之前都是靠synchronized
關鍵字保證同步的,這種通過使用一致的鎖定協議來協調對共享狀態的訪問,可以確保無論哪個線程持有共享變量的鎖,都采用獨占的方式來訪問這些變量。獨占鎖其實就是一種悲觀鎖,所以可以說synchronized
是悲觀鎖。
悲觀鎖機制存在以下問題:
在多線程競爭下,加鎖、釋放鎖會導致比較多的上下文切換和調度延時,引起性能問題。
一個線程持有鎖會導致其它所有需要此鎖的線程掛起。
如果一個優先級高的線程等待一個優先級低的線程釋放鎖會導致優先級倒置,引起性能風險。
而另一個更加有效的鎖就是樂觀鎖。所謂樂觀鎖就是,每次不加鎖而是假設沒有沖突而去完成某項操作,如果因為沖突失敗就重試,直到成功為止。
與鎖相比,volatile
變量是一個更輕量級的同步機制,因為在使用這些變量時不會發生上下文切換和線程調度等操作,但是volatile
不能解決原子性問題,因此當一個變量依賴舊值時就不能使用volatile
變量。因此對於同步最終還是要回到鎖機制上來。
樂觀鎖
樂觀鎖( Optimistic Locking
)其實是一種思想。相對悲觀鎖而言,樂觀鎖假設認為數據一般情況下不會造成沖突,所以在數據進行提交更新的時候,才會正式對數據的沖突與否進行檢測,如果發現沖突了,則讓返回用戶錯誤的信息,讓用戶決定如何去做。
上面提到的樂觀鎖的概念中其實已經闡述了他的具體實現細節:主要就是兩個步驟:沖突檢測和數據更新。其實現方式有一種比較典型的就是Compare and Swap(CAS
)。
CAS
CAS是項樂觀鎖技術,當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程並不會被掛起,而是被告知這次競爭中失敗,並可以再次嘗試。
CAS 操作包含三個操作數 —— 內存位置(V)、預期原值(A)和新值(B)。如果內存位置的值與預期原值相匹配,那麽處理器會自動將該位置值更新為新值。否則,處理器不做任何操作。無論哪種情況,它都會在 CAS 指令之前返回該位置的值。(在 CAS 的一些特殊情況下將僅返回 CAS 是否成功,而不提取當前值。)CAS 有效地說明了“我認為位置 V 應該包含值 A;如果包含該值,則將 B 放到這個位置;否則,不要更改該位置,只告訴我這個位置現在的值即可。”這其實和樂觀鎖的沖突檢查+數據更新的原理是一樣的。
這裏再強調一下,樂觀鎖是一種思想。CAS是這種思想的一種實現方式。
Java對CAS的支持
在JDK1.5 中新增java.util.concurrent
(J.U.C)就是建立在CAS之上的。相對於對於synchronized
這種阻塞算法,CAS是非阻塞算法的一種常見實現。所以J.U.C在性能上有了很大的提升。
我們以java.util.concurrent
中的AtomicInteger
為例,看一下在不使用鎖的情況下是如何保證線程安全的。主要理解getAndIncrement
方法,該方法的作用相當於 ++i
操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class AtomicInteger extends Number implements java.io.Serializable {
private volatile int value;
public final int get() {
return value;
}
public final int getAndIncrement() {
for (;;) {
int current = get();
int next = current + 1 ;
if (compareAndSet(current, next))
return current;
}
}
public final boolean compareAndSet( int expect, int update) {
return unsafe.compareAndSwapInt( this , valueOffset, expect, update);
}
}
|
在沒有鎖的機制下需要字段value要借助volatile原語,保證線程間的數據是可見的。這樣在獲取變量的值的時候才能直接讀取。然後來看看++i
是怎麽做到的。
getAndIncrement
采用了CAS操作,每次從內存中讀取數據然後將此數據和+1
後的結果進行CAS操作,如果成功就返回結果,否則重試直到成功為止。而compareAndSet
利用JNI來完成CPU指令的操作。
ABA問題
CAS會導致“ABA問題”。
CAS算法實現一個重要前提需要取出內存中某時刻的數據,而在下時刻比較並替換,那麽在這個時間差類會導致數據的變化。
比如說一個線程one從內存位置V中取出A,這時候另一個線程two也從內存中取出A,並且two進行了一些操作變成了B,然後two又將V位置的數據變成A,這時候線程one進行CAS操作發現內存中仍然是A,然後one操作成功。盡管線程one的CAS操作成功,但是不代表這個過程就是沒有問題的。
部分樂觀鎖的實現是通過版本號(version
)的方式來解決ABA問題,樂觀鎖每次在執行數據的修改操作時,都會帶上一個版本號,一旦版本號和數據的版本號一致就可以執行修改操作並對版本號執行+1
操作,否則就執行失敗。因為每次操作的版本號都會隨之增加,所以不會出現ABA問題,因為版本號只會增加不會減少。
總結
Java中的線程安全問題至關重要,要想保證線程安全,就需要鎖機制。鎖機制包含兩種:樂觀鎖與悲觀鎖。悲觀鎖是獨占鎖,阻塞鎖。樂觀鎖是非獨占鎖,非阻塞鎖。有一種樂觀鎖的實現方式就是CAS ,這種算法在JDK 1.5中引入的java.util.concurrent
中有廣泛應用。但是值得註意的是這種算法會存在ABA問題。
CAS與對象創建
另外,CAS還有一個應用,那就是在JVM創建對象的過程中。對象創建在虛擬機中是非常頻繁的。即使是僅僅修改一個指針所指向的位置,在並發情況下也不是線程安全的,可能正在給對象A分配內存空間,指針還沒來得及修改,對象B又同時使用了原來的指針來分配內存的情況。解決這個問題的方案有兩種,其中一種就是采用CAS配上失敗重試的方式保證更新操作的原子性。
參考資料
非阻塞同步算法與CAS(Compare and Swap)無鎖算法
CAS原理分析
Java CAS 和ABA問題
樂觀鎖的一種實現方式——CAS