1. 程式人生 > >演算法:CAS演算法。

演算法: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)));
}

相關推薦

no