併發策略-CAS算法

對於併發控制而言,我們平時用的鎖(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

avatar

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章