對於併發控制而言,我們平時用的鎖(synchronized,Lock)是一種悲觀的策略。它總是假設每一次臨界區操作會產生衝突,因此,必須對每次操作都小心翼翼。如果多個線程同時訪問臨界區資源,就寧可犧牲性能讓線程進行等待,所以鎖會阻塞線程執行。 與之相對的有一種樂觀的策略,它會假設對資源的訪問是沒有沖 ...
對於併發控制而言,我們平時用的鎖(synchronized,Lock)是一種悲觀的策略。它總是假設每一次臨界區操作會產生衝突,因此,必須對每次操作都小心翼翼。如果多個線程同時訪問臨界區資源,就寧可犧牲性能讓線程進行等待,所以鎖會阻塞線程執行。
與之相對的有一種樂觀的策略,它會假設對資源的訪問是沒有衝突的。既然沒有衝突也就無需等待了,所有的線程都在不停頓的狀態下持續執行。那如果遇到問題了無鎖的策略使用一種叫做比較交換(CAS Compare And Swap)來鑒別線程衝突,一旦檢測到衝突產生,就重試當前操作直到沒有衝突。CAS演算法是非阻塞的,它對死鎖問題天生免疫,而且它比基於鎖的方式擁有更優越的性能。
CAS演算法的過程是這樣:它包含三個參數 CAS(V,E,N)。V表示要更新的變數,E表示預期的值,N表示新值。僅當V值等於E值時,才會將V的值設置成N,否則什麼都不做。最後CAS返回當前V的值。CAS演算法需要你額外給出一個期望值,也就是你認為現在變數應該是什麼樣子,如果變數不是你想象的那樣,那說明已經被別人修改過。你就重新讀取,再次嘗試修改即可。
JDK併發包有一個atomic包,裡面實現了一些直接使用CAS操作的線程安全的類型。其中最常用的一個類應該就是AtomicInteger。我們以此為例來研究一下沒有鎖的情況下如何做到線程安全。
private volatile int value;
這是AtomicInteger類的核心欄位,代表當前實際取值,藉助volatile保證線程間數據的可見性。
獲取內部數據的方法:
public final int get() {
return value;
}
我們從源碼的實現看看incrementAndGet()
的內部實現
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
代碼第二行使用了一個死迴圈,原因是:
CAS的操作未必都是成功的,因此對於不成功的情況,我們就需要進行不斷的嘗試。
第三行取得當前值,接著+1得到新值next。
這裡我們使用CAS必需的兩個參數:期望值以及新值。
使用compareAndSet()將新值next寫入。成功的條件是在寫入的時刻當前的值應該要等於剛剛取到的current。如果不是這樣則說明AtomicInteger的值在第3行到第5行之間被其他線程修改過了。當前看到的狀態是一個過期的狀態,因此返回失敗,需要進行下一次重試,直到成功為止。
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
整體的過程就是這樣子,利用CPU的CAS指令,同時藉助JNI來完成Java的非阻塞演算法。其它原子操作都是利用類似的特性完成的。大概的邏輯應該是這樣:
if (this == expect) {
this = update return true;
} else {
return false;
}
CAS雖然能高效的解決原子問題,但是CAS也會帶來1個經典問題即ABA問題:
因為CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。
ABA問題的解決思路就是使用版本號。在變數前面追加上版本號,每次變數更新的時候把版本號加一,那麼A-B-A 就會變成1A-2B-3A。
從Java1.5開始JDK的atomic包里提供了一個類AtomicStampedReference來解決ABA問題。這個類在內部不僅維護了對象值,還維護了一個時間戳(可以是任意的一個整數來表示狀態值)。當設置對象值時,對象值和狀態值都必須滿足期望值才會寫入成功。因此即使對象被反覆讀寫,寫會原值,只要狀態值發生變化,就能防止不恰當的寫入。
/**
* @param expectedReference 期望值
* @param newReference 寫入新值
* @param expectedStamp 期望狀態值
* @param newStamp 新狀態值
* @return true if successful
*/
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)));
}
個人公眾號:JAVA日知錄 , javadaily.cn