CAS操作在Java中的應用很廣泛,比如ConcurrentHashMap
,ReentrantLock
等,其常被用來解決獨佔鎖對線程阻塞而導致的性能低下問題,是高效併發必備的一種優化方法.
JMM
一般的理解Java內存模型爲主內存與工作內存,如下圖所示:
工作內存是爲了提高效率,在內部緩存了主內存中的變量,避免每次都要去主內存拿,但是變量被修改之後寫回主內存的時機是不可控的,因此就會帶來併發下變量一致性問題.對此Java提供了以下關鍵字:
volatile: 保證多線程之間的可見性,可以理解爲其操作都是直接操作主內存的變量,每次讀直接從主內存讀,每次修改完立即寫回主內存. synchronized: 提供的鎖機制在進入同步塊時從主內存讀取變量,同步塊結束時寫回變量到主內存.
synchronized所帶來的新問題.
這裏的分析是不考慮JVM一系列的優化措施,比如鎖消除,鎖粗化,自旋之類的處理優化. 排除優化措施的話synchronized本質上可以理解爲悲觀鎖思想的實現,所謂的悲觀鎖認爲每次訪問臨界區都會衝突,因此每次都需要加鎖,而當沒有拿到鎖時線程是處於阻塞狀態的.從Runnable到Blocked,然後被喚醒後再從Blocked到Runnable,這些操作耗費了不少計算機資源,因此這種悲觀鎖機制是併發的一種實現,但不是高效併發的實現.
CAS實現原子性操作
CAS操作大概有如下幾步:
- 讀取舊值爲一個臨時變量
- 對舊值的臨時變量進行操作或者依賴舊值臨時變量進行一些操作
- 判斷舊值臨時變量是不是等於舊值,等於則沒被修改,那麼新值寫入.不等於則被修改,此時放棄或者從步驟1重試.
那麼步驟三實際上就是比較並替換,這個操作需要是原子性的,不然無法保證比較操作之後還沒寫入之前有其他線程操作修改了舊值.那麼這一步實際上就是CAS(CompareAndSwap),其是需要操作系統底層支持,對於操作系統會轉換爲一條指令,也就是自帶原子性屬性,對於Java中則是sun.misc.Unsafe#compareAndSwapObject
這種類型的操作.另外在Java中CAS的實現需要可見性的支持,也就是修改值後必須立即同步到主內存,否則這個修改沒有意義,其他線程讀取到的仍然是舊值.
CAS相比無優化下的synchronized,最大的優勢就是無阻塞,也就是沒了線程阻塞與喚醒的消耗,性能自然是很高.
AtomicXXX與CAS
Java中提供了AtomicXXX
一系列原子類,這裏以AtomicInteger
爲例,大概結構如下:
public class AtomicInteger extends Number implements java.io.Serializable { private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; // 使用volatile修飾,解決可見性問題 private volatile int value; }
這些類一般內部包裹一個用volatile
修飾的真實值,其解決的是內存可見性與指令重排序的問題.而原子性操作則是由unsafe
提供的一系列指令來完成.以java.util.concurrent.atomic.AtomicInteger#getAndIncrement
爲例,其解決的是i++的問題,在AtomicInteger
中對於此類操作都轉到了unsafe的操作.
public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); }
Unsafe中實現的策略,爲了更好的理解筆者調整了一些代碼順序.正好對應上述CAS的三個步驟.
public final int getAndAddInt(Object var1, long valueOffset, int addValue) { int expect; do { // 操作1 得到舊值 expect = this.getIntVolatile(var1, valueOffset); // 操作1 計算新值 int newValue = expect + addValue; // 操作3,比較,如果舊值沒改變則更新其爲新值,否則重試.這種實現也被成爲自旋CAS } while(!this.compareAndSwapInt(var1, valueOffset, expect, newValue)); return expect; }
ReentrantLock與CAS
ReentrantLock
是Java應用層面實現的一種獨佔鎖機制,因此比起JDK1.5之前的synchronized
有很明顯的性能提升.其加鎖的代碼利用的就是CAS算法.其內部利用了一個state
字段,該字段爲0時代表鎖沒有被獲取,爲1時則代表有線程已經獲取到了鎖,爲n時則代表該鎖被當前線程重入了n次.
final void lock() { // 判斷當前對象所處的狀態,爲0則鎖沒被獲取,因此當前線程獨佔,並修改state爲1.那麼進來的其他線程加入到等待隊列中. if (compareAndSetState(0, 1)) // 當前線程獨佔 setExclusiveOwnerThread(Thread.currentThread()); else // 其他線程排隊 acquire(1); }
那麼可重入機制是怎麼實現的呢?
在acquire(1)
方法中會調用tryAcquire(1)
方法再次嘗試獲得鎖,其又會轉到java.util.concurrent.locks.ReentrantLock.Sync#nonfairTryAcquire
方法,在這個方法中重入的關鍵就是對state自增,state爲n就代表重入了n次.
final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); // c == 0 則代表鎖已經被釋放,因此直接獲取並獨佔即可 if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } // 重入實現的關鍵點,當前線程等於已獲得獨佔鎖的線程 else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); // 設置新的state,假設state爲2就代表被當前線程重入了兩次. setState(nextc); return true; } return false; }
那麼鎖的釋放實際上就是對state
字段的遞減,並且當減到0時對等待隊列中的線程進行喚醒.
public final boolean release(int arg) { // tryRelease會對state字段進行操作 if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) // 喚醒其他線程 unparkSuccessor(h); return true; } return false; }
tryRelease
是對state字段的遞減過程.
protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; // 當減爲0,代表鎖已經空閒,因此要釋放獨佔線程. if (c == 0) { free = true; setExclusiveOwnerThread(null); } // 更新新的state setState(c); return free; }
簡單總結來說ReentrantLock
實現獨佔的重入鎖是通過CAS對state
變量的改變來代表不同的狀態來實現的,從而實現了獲取鎖與釋放鎖的高性能.
CAS總結
CAS在多線程問題中起到了什麼作用?
多線程問題歸根結底要解決的是可見性
,有序性
,原子性
三大問題,大家都知道JVM提供的volatile
可以保證可見性與有序性,但是無法保證原子性,換句話說 volatile + CAS實現原子性操作 = 線程安全 = 高效併發
,那麼CAS就是用來實現這個操作的原子性.
CAS與樂觀鎖是什麼關係?
樂觀鎖是一種思想,其認爲衝突很少發生,因此只在最後寫操作的時候加鎖
,這裏的加鎖不一定是真的鎖上,比如CAS一般就用來實現這一層加鎖.
ABA問題
ABA問題指的是多個線程同時執行,那麼開始時其獲得的值都是A,當一個線程修改了A爲B,第二個線程修改了B爲A,那麼第三個線程修改時判斷A仍然是A,認爲其沒有修改過,因此會CAS成功.
ABA問題產生的影響取決於你的業務是否會因此受到影響.如果有影響那麼解決思路一般是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加一,那麼A-B-A 就會變成1A-2B-3A。
在JDK1.5之後提供了AtomicStampedReference
類來解決ABA問題,解決思路是保存元素的引用,引用相當於版本號,是每一個變量的標識,因此在CAS前判斷下是否是同一個引用即可.
public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) { Pair<V> current = pair; return expectedReference == current.reference && expectedStamp == current.stamp && ((newReference == current.reference && newStamp == current.stamp) || casPair(current, Pair.of(newReference, newStamp))); }
如有錯誤,還請指出,以免誤人子弟.