從concurrent下的Atomic原子類說起

vipshop_fin_dev發表於2018-07-02

前言

在使用多執行緒併發的時候,我們經常會使用到JDK1.5版本開始引入的原子處理類,如AtomicInteger、AtomicBoolean、AtomicReference、AtomicStampedReference等。
本文將從AtomicInteger的實現出發,探討多執行緒下的無鎖實現及相關的知識,包括樂觀鎖與悲觀鎖及CAS演算法思想。


AtomicInteger介紹

AtomicInteger是什麼

AtomicInteger是一個提供原子操作的Integer類,通過執行緒安全的方式操作加減或設值。

AtomicInteger的使用場景

AtomicInteger提供原子操作來進行Integer的操作,因此十分適合高併發情況下使用。

AtomicInteger的主要API

public final int get(); // 取得當前值
public final void set(int newValue);    // 設定當前值
public final int getAndSet(int newValue);   // 設定新值,並返回舊值
public final boolean compareAndSet(int expect, int u);  // 如果當前值為expect,則設定為u
public final int getAndIncrement(); // 當前值加1,返回舊值,相當於i++
public final int incrementAndGet(); // 當前值加1,返回新值,相當於++i
public final int addAndGet(int delta);  // 當前值增加delta,返回新值
public final int getAndDecrement(); // 當前值減1,返回舊值,相當於i--
public final int decrementAndGet(); // 當前值減1,返回新值,相當於--i
public final int getAndAdd(int delta);  // 當前值增加delta,返回舊值

AtomicInteger的原始碼解析

以AtomicInteger.getAndIncrement()為例,在JDK1.8上的原始碼實現如下:

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

可以看出AtomicInteger直接呼叫了Unsafe中的getAndAddInt()方法。

Unsafe類簡介

Unsafe類是在sun.misc包下,不屬於Java標準。但是很多Java的基礎類庫,包括一些被廣泛使用的高效能開發庫都是基於Unsafe類開發的,比如Netty、Cassandra、Hadoop、Kafka等。Unsafe類在提升Java執行效率,增強Java語言底層操作能力方面起了很大的作用。
Unsafe類使Java擁有了像C語言的指標一樣操作記憶體空間的能力,同時也帶來了指標的問題。過度的使用Unsafe類會使得出錯的機率變大,因此Java官方並不建議使用的,官方文件也幾乎沒有。
UnSafe類提供了很多底層的操作,包括記憶體管理、非常規的物件例項化、陣列操作、多執行緒同步(包括鎖機制、CAS操作)、掛起與恢復等操作。
繼續進入Unsafe類中檢視

public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!compareAndSwapInt(o, offset, v, v + delta));
    return v;
}
public native int getIntVolatile(Object o, long offset);
public final native boolean compareAndSwapInt(Object o, long offset,
                                              int expected,
                                              int x);

這裡可以看出,實現執行緒安全的getAndIncrement()是通過while迴圈裡不停地呼叫native的getIntVolatile()方法,直到compareAndSwapInt()方法返回true為止。
為什麼設定一個值需要在一個迴圈裡面不停地執行?原因就是這裡應用了併發設計中的CAS演算法。

悲觀鎖、樂觀鎖及CAS的思想

我們平時用的鎖(synchronized,Lock)是一種悲觀的策略。它總是假設每一次臨界區操作會產生衝突。因此,必須對每次操作都小心翼翼。如果多個執行緒同時訪問臨界區資源,就寧可犧牲效能讓執行緒進行等待,所以鎖會阻塞執行緒執行。這裡稱之為悲觀鎖。

與之相對的有一種樂觀的策略,我們可以稱之為無鎖,也即樂觀鎖
在無鎖的呼叫中,一個典型的特點是可能包含一個無窮迴圈。在這個迴圈中,執行緒會不斷嘗試修改共享變數。如果沒有衝突,修改成功,那麼退出迴圈,否則繼續嘗試修改。但無論如何,無鎖的並行總能保證有一個執行緒是可以勝出,不至於全軍覆沒。至於臨界區中競爭失敗的執行緒,它們則必須不斷重試,直到自己獲勝。如果運氣很不好,總是嘗試不成功,就會出現類似飢餓的現象,執行緒停滯不前。

CAS(Compare and swap)即比較交換,是設計併發演算法時用到的一種技術,可以用它來實現無鎖。簡單來說,CAS使用一個期望值和一個變數的當前值進行比較,如果當前變數的值與我們期望的值相等,就使用一個新值替換當前變數的值。
由於CAS演算法是非阻塞的,它對死鎖問題天生免疫,而且它比基於鎖的方式擁有更優越的效能

CAS的實現

CAS演算法的過程是這樣:它包含三個引數 CAS(V,E,N)。V表示要更新的變數,E表示預期的值,N表示新值。僅當V值等於E值時,才會將V的值設定成N,否則什麼都不做。最後CAS返回當前V的真實值。CAS操作是抱著樂觀的態度進行,它總是認為自己可以成功完成操作。當多個執行緒同時使用CAS操作一個變數時,只有一個會勝出,併成功更新,其餘均會失敗。失敗的執行緒不會被掛起,僅是被告知失敗,並且允許再次嘗試。
在硬體層面,大部分的現代處理器都已經支援原子化的CAS指令。

CAS的缺點-ABA問題

執行緒判斷被修改物件是否可以正確寫入的條件是物件的當前值和期望值是否一致。這個邏輯從一般意義上來說是正確的。但是可能有一個小小的例外:當你獲得物件當前資料後,在準備修改為新值前,物件的值被其他執行緒連續修改了兩次,而經過這兩次修改後,物件的值又恢復為舊值。這樣,當前執行緒就無法判斷這個物件究竟是否被修改過。此時有可能會導致意想不到的結果。這就是ABA問題
比如兩個執行緒
執行緒1 查詢A的值為a,與舊值a比較,
執行緒2 查詢A的值為a,與舊值a比較,相等,更新為b值
執行緒2 查詢A的值為b,與舊值b比較,相等,更新為a值
執行緒1 相等,更新B的值為c
可以看到這樣的情況下,執行緒1 可以正常 進行CAS操作,將值從a變為c 但是在這之間,實際A值已經發了a->b b->a的轉換。
為此可引入版本號或時間戳來解決這個問題。例如在JDK中的AtomicStampedReference類,它的內部不僅維護了物件值,還維護了一個時間戳。當AtomicStampedReference的值被修改時,除了更新資料本身外,還必須更新時間戳。當設定值時,只有物件值及時間戳都滿足期望值時,寫入才成功。

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)));
}

private boolean casPair(Pair<V> cmp, Pair<V> val) {
        return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}

後記

JDK中的Atomic原子類使用了CAS實現多執行緒併發操作。相比使用互斥鎖,CAS實現的是自旋鎖,有著較好的效能,但也有它的侷限性及適用場景。
對於資源競爭較少(執行緒衝突較輕)的情況,使用synchronized同步鎖進行執行緒阻塞和喚醒切換以及使用者態核心態間的切換操作額外浪費消耗CPU資源;而CAS基於硬體實現,不需要進入核心,不需要切換執行緒,自旋的機率較少,因此可以獲得更高的效能。
對於資源競爭嚴重(執行緒衝突嚴重)的情況,CAS自旋的概率會比較大,從而浪費更多的CPU資源,效率低於synchronized。
關於自旋鎖與互斥鎖的對比和優缺點,仍有待後續學習研究。本次由Atomic原子類為楔子所帶起的相關知識就介紹到此,CAS更底層的實現可參考部落格的另外一篇文章:https://mp.csdn.net/mdeditor/79415104


Written By 歐曉星

相關文章