1. 程式人生 > >java併發之原子操作類(AtomicLong原始碼分析)和非阻塞演算法

java併發之原子操作類(AtomicLong原始碼分析)和非阻塞演算法

 

背景

近年來,在併發演算法領域的大多數研究都側重於非阻塞演算法,這種演算法用底層的原子機器指令(例如比較併發交換指令)代替鎖來確保資料在併發訪問中的一致性。非阻塞演算法被廣泛的用於在作業系統和JVM中實現執行緒/程序排程機制、垃圾回收機制以及鎖和其他併發資料結構。

與基於鎖的方案相比,非阻塞演算法在設計和實現上都要複雜的多,但他們在可伸縮性和活躍性上卻擁有巨大的優勢,由於非阻塞演算法可以使多個執行緒在競爭相同資料時不會發生阻塞,因此它能在粒度更細的層次上面進行協調,並且極大的減少排程開銷。鎖雖然Java語言鎖定語法比較簡潔,但JVM操作和管理鎖時,需要完成的工作缺並不簡單,在實現鎖定時需要遍歷JVM中一條複雜的程式碼路徑,並可能導致作業系統級的鎖定、執行緒掛起以及上下文切換等操作。

非阻塞演算法

在基於鎖的演算法中可能會發生各種活躍性故障,如果執行緒在持有鎖時由於阻塞I/O,記憶體頁缺失或其他延遲執行,那麼很可能所有執行緒都不能繼續執行下去。如果在某種演算法中,一個執行緒的失敗或掛起不會導致其他執行緒也失敗或掛起,那麼這種演算法就稱為非阻塞演算法。如果在演算法的每個步驟中都存在某個執行緒能夠執行下去,那麼這種演算法也被稱為無鎖演算法。如果在演算法中僅將CAS用於協調執行緒之間的操作,並且能夠正確的實現,那麼他既是一種無阻塞演算法,又是一種無鎖演算法。

Java對非阻塞演算法的支援:從Java5.0開始,底層可以使用原子變數類(例如AtomicInteger和AtoMicReference)來構建高效的非阻塞演算法,底層實現採用的是一個比較並交換指令(CAS)。

比較並交換(CAS)

CAS包括了三個運算元,需要讀寫的記憶體位置V,進行比較的值A和擬寫入的新值B。當且僅當V的值等於A時,CAS才會通過原子方式用新值B來更新A的值,否則不會執行任何操作。無論V的值是否等於A,都將返回V原有的值。CAS的含義是:我認為V的值應該是A,如果是那麼將V的值更新為B,否則不修改並告訴V的值實際為多少。

 

原子變數類

原子變數(對應記憶體模型中的原子性)比鎖的粒度更細。量級更輕,並且對於在多處理器系統上實現高效能的併發程式碼來說是非常關鍵的。原子變數將發生競爭的範圍縮小到單個變數上面,這是你獲得的粒度最細的情況。更新原子變數的快速(非競爭)路徑不會被獲得鎖的快速路徑慢,並且通常會更快,而它的慢速路徑肯定比鎖的慢速路徑塊,因為他不需要掛起或者重新排程執行緒。在使用基於原子變數而非鎖的演算法中,執行緒在執行時更不易出現延遲,並且如果遇到競爭,也更容易恢復過來。

 

 

Java中的13個原子操作類

Java從JDK1.5開始提供了java.util.concurrent.atomic包(以下簡稱Atomic包),這個包中的原子操作類提供了一種用法簡單、效能高效、執行緒安全地更新一個變數的方式。因為變數的型別有很多種,所以在Atomic包裡一共提供了13個類,屬於4種類型的原子更新方式,分別是原子更新基本型別、原子更新陣列、原子更新引用和原子更新屬性(欄位)。Atomic包裡的類基本都是使用Unsafe實現的包裝類。

  • 原子更新基本型別類

使用原子的方式更新基本型別,Atomic包提供了以下3個類。

  1. AtomicBoolean:原子更新布林型別。
  2. AtomicInteger:原子更新整型。
  3. AtomicLong:原子更新長整型。
  • 原子更新陣列

通過原子的方式更新數組裡的某個元素,Atomic包提供了以下4個類。

  1. AtomicIntegerArray:原子更新整型數組裡的元素。
  2. AtomicLongArray:原子更新長整型數組裡的元素。
  3. AtomicReferenceArray:原子更新引用型別數組裡的元素。
  • 原子更新引用型別

原子更新基本型別的AtomicInteger,只能更新一個變數,如果要原子更新多個變數,就需要使用這個原子更新引用型別提供的類。Atomic包提供了以下3個類。

  1. AtomicReference:原子更新引用型別。
  2. AtomicReferenceFieldUpdater:原子更新引用型別裡的欄位。
  3. AtomicMarkableReference:原子更新帶有標記位的引用型別。可以原子更新一個布林型別的標記位和引用型別。構造方法是AtomicMarkableReference(V initialRef,booleaninitialMark)。
  • 原子更新欄位類

如果需原子地更新某個類裡的某個欄位時,就需要使用原子更新欄位類,Atomic包提供了以下3個類進行原子欄位更新。

  1. AtomicIntegerFieldUpdater:原子更新整型的欄位的更新器。
  2. AtomicLongFieldUpdater:原子更新長整型欄位的更新器。
  3. AtomicStampedReference:原子更新帶有版本號的引用型別。該類將整數值與引用關聯起
  4. 來,可用於原子的更新資料和資料的版本號,可以解決使用CAS進行原子更新時可能出現的
  5. ABA問題。

AtomicLong原始碼分析

上面的4種原子型別都是基於CAS實現,低層藉助於unsafe實現原子操作。接下來結合原始碼,看一下比較有代表性的AtomicLong原始碼

初始化

//儲存AtomicLong的實際值,用volatile 修飾保證可見性
private volatile long value;

// 獲取value的記憶體地址的邏輯操作
    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicLong.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }


//根據傳入的引數初始化實際值,預設值為0
public AtomicLong(long initialValue) {
        value = initialValue;
    }

接下來我們主要看一下幾個更新方法

//以原子方式更新值為傳入的newValue,並返回更新之前的值
public final long getAndSet(long newValue) {
        return unsafe.getAndSetLong(this, valueOffset, newValue);
    }


//輸入期望值和更新值,如果輸入的值等於預期值,則以原子方式更新該值為輸入的值
public final boolean compareAndSet(long expect, long update) {
        return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
    }

//返回當前值原子加1後的值
public final long getAndIncrement() {
        return unsafe.getAndAddLong(this, valueOffset, 1L);
    }

//返回當前值原子減1後的值
public final long getAndDecrement() {
        return unsafe.getAndAddLong(this, valueOffset, -1L);
    }

//返回當前值原子增加delta後的值
public final long getAndAdd(long delta) {
        return unsafe.getAndAddLong(this, valueOffset, delta);
    }

上面列出來主要用的一些方法,可以看出基本都是呼叫unsafe.getAndAddLong方法,接下來我們具體看下


public native long getLongVolatile(Object var1, long var2); 


public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

/*
unsafe.getAndAddLong(this, valueOffset, 1L)
var1 當前值
var2 value值在AtomicLong物件中的記憶體偏移地址

*/

public final long getAndAddLong(Object var1, long var2, long var4) {
        long var6;
        do {
            //根據var1和var2得出當前變數的值,以便接下來執行更新操作
            var6 = this.getLongVolatile(var1, var2);

            //如果當前值為var6,則將值加var4,這樣做是確保每次更新時,變數的值是沒有被其他線
//程修改過的值,如果被修改,則重新獲取最新值更新,直到更新成功
        } while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));

        return var6;
    }

從原始碼可以看出,獲取當前值getLongVolatile方法,比較並交換compareAndSwapLong方法都是native方法。說明不是採用java實現原子操作的,具體各位同學可以繼續去檢視底層原始碼(應該是c++)實現,這裡不在深入了(能力有限)。

比較並交換的缺陷

1、通過原始碼可以看出,原子更新時,會先獲取當前值,確保當前值沒被修改過後在進行更新操作,這也意味著如果競爭十分激烈,CAS的效率是有可能比鎖更低的(一般在實際中不會出現這種情況),JDK後面推出了LongAdd,粒度更小,競爭也會被分散到更低,具體實現各位同學可以自行了解。

2、ABA是談到CAS不可避免的話題,比較並交換,會存在這樣一個場景,當變數為值A時,將值執行更新。然而在實際中,有可能其他執行緒將值先改為B,然後又將值改回A,此時還是能夠成功執行更新操作的(對於某些不在乎過程的沒啥影響,對於連結串列之類的就不滿足了)。解決方式是給變數打上版本號,如果版本號和值一致才執行更新操作(可使用AtomicReference)。

 

總結

非阻塞演算法通過底層的併發原語(例如比較交換而不是鎖)來維持執行緒的安全性。這些底層的原語通過原子變數類向外公開,這些類也用做一種“更好的volatile變數”,從而為整數和物件引用提供原子的更新操作。

參考書籍:

《JAVA併發程式設計實戰》

《JAVA併發程式設計的藝術》