Atomic原子類及原理

目錄

1 前言

2 unsafe類對Atomic原子類的支持

3 AtomicInteger的內部實現

3.1 準備

3.2 讀

3.3 寫

4 CAS機制

4.1 基本操作數

4.2 例子

4.3 缺點


1 前言

當一個線程更新一個變量時,程序如果沒有正確的同步,那麼這個變量對於其他線程來說是不可見的。我們通常使用synchronized或者volatile來保證線程安全的更新共享變量。在JDK1.5中,提供了java.util.concurrent.atomic包,這個包中的原子操作類提供了一種用法簡單,性能高效,線程安全地更新一個變量的方式。

Atomic包裏一共提供了13個類,有4種類型的原子更新方式:原子更新基本類型(如AtomicInteger)、原子更新數組(如AtomicIntegerArray)、原子更新引用(如AtomicReference)和原子更新屬性(如AtomicIntegerFieldUpdater)。

2 unsafe類對Atomic原子類的支持

Java中的Unsafe類爲我們提供了類似C++手動管理內存的能力,從名字中我們可以看出來這個類對普通程序員來說是“危險”的,一般應用開發者不會用到這個類。unsafe可以參考:Java中的unsafeAtomic原子類的基本都是使用unsafe類實現的,對原子類的操作基本有3個方面:準備、讀、寫。

  • 準備:unsafe類提供了相關方法來獲取實例中值的偏移量,通過該偏移量unsafe類可以直接對原子類內存地址中的值進行獲取和修改。
public native long staticFieldOffset(Field var1);
 
public native long objectFieldOffset(Field var1);
 
public native Object staticFieldBase(Field var1);
 
public native int arrayBaseOffset(Class<?> var1);
 
public native int arrayIndexScale(Class<?> var1);

staticFieldOffset方法用於獲取靜態屬性Field在對象中的偏移量,讀寫靜態屬性時必須獲取其偏移量。objectFieldOffset方法用於獲取非靜態屬性Field在對象實例中的偏移量,讀寫對象的非靜態屬性時會用到這個偏移量。staticFieldBase方法用於返回Field所在的對象。arrayBaseOffset方法用於返回數組中第一個元素實際地址相對整個數組對象的地址的偏移量。arrayIndexScale方法用於計算數組中第一個元素所佔用的內存空間。

  • 讀:對於原子類中存儲的值,unsafe提供了方法來對內存地址中的值進行volatile級別的獲取。
// getIntVolatile方法用於在對象指定偏移地址處volatile讀取一個int
public native int getIntVolatile(Object var1, long var2);
  • 寫:unsafe提供了3種CAS操作(樂觀鎖),來對原子類進行修改。
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
 
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
 
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

在Java的基本類型中除了Atomic包中提供原子更新的基本類型外,還有char、float和double。那麼這些在Atomic包中沒有提供原子更新的基本類型怎麼保證其原子更新呢?

從AtomicBoolean源碼中我們可以得到答案:首先將Boolean轉換爲整型,然後使用comareAndSwapInt進行CAS,所以原子更新char、float、double同樣可以以此實現。

3 AtomicInteger的內部實現

以AtomicInteger爲例來看一下上面的實現過程。

3.1 準備

下面靜態代碼塊中的objectFieldOffset方法,獲取value屬性在AtomicInteger實例中的偏移量valueOffset,後面操作value時必須要用到這個偏移量。

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value;

這段代碼我們需要注意一下幾個方面:

  • unsafe字段,AtomicInteger包含了一個Unsafe類的實例,unsafe就是用來實現CAS機制的,CAS機制我們在後面會講到;
  • value字段,表示當前對象代碼的基本類型的值,AtomicInteger是int型的線程安全包裝類,value就代碼了AtomicInteger的值。注意,這個字段是volatile的。
  • valueOfset,指是value在內存中的偏移量,也就是在內存中的地址,通過Unsafe.objectFieldOffset(Field f)獲取。這個值在使用CAS機制的時候會用到。
  • objectFieldOffset方法用於獲取非靜態屬性Field在對象實例中的偏移量,讀寫對象的非靜態屬性時會用到這個偏移量。

3.2 讀

AtomicInteger本身提供了get()來獲取value值,該方法直接返回value變量。因爲value變量是volatile類型,這就保證get()讀到的是最新值,因爲是直接從主內存中讀取value 。

private volatile int value;           
// volatile保證get()讀到的是最新值,因爲直接從主內存中讀取value 
public final int get() {    return value;    }

再來看AtomicInteger的getAndAdd(),直接調用的unsafe類的getAndAddInt()。可以看到調用了unsafe.getIntVolatile()和unsafe.compareAndSwapInt()兩個方法。

    // AtomicInteger方法。將value設置爲delta,返回修改之前的值
    public final int getAndAdd(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }

    // unsafe方法。當使用CAS修改成功時才返回值
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);    // 獲取AtomicInteger的value
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

3.3 寫

上面介紹讀的時候也看到了AtomicInteger.getAndAdd()中使用到了unsafe.compareAndSwapInt()。下面我們來看一個AtomicInteger類中的主要方法getAndIncrement(),也就相當於i++操作,只不過它是線程安全的,其實現代碼如下:

public final int getAndIncrement() {
         for (;;) {
             int current = get();
             int next = current + 1;
             if (compareAndSet(current, next))
                 return current;
         }
 }

       這個方法的做法爲先獲取到當前的 value 屬性值,然後將 value 加 1,賦值給一個局部的 next 變量,然而,這兩步都是非線程安全的,但是內部有一個死循環,不斷去做compareAndSet操作,直到成功爲止,也就是修改的根本在compareAndSet方法裏面。compareAndSet()方法的代碼如下:

public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
 }

      compareAndSet 傳入的爲執行方法時獲取到的 value 屬性值,update爲加 1 後的值, compareAndSet所做的爲調用 Sun 的 UnSafe 的 compareAndSwapInt方法來完成,此方法爲 native 方法,compareAndSwapInt 基於的是CPU 的 CAS指令來實現的。下面我們將詳細的來介紹一下CAS的實現原理。

4 CAS機制

CAS是英文單詞Compare And Swap的縮寫,翻譯過來就是比較並替換。

4.1 基本操作數

CAS機制當中使用了3個基本操作數:

(1)內存地址V,也就是AtomicInteger中的valueOffset。

(2)舊的預期值A,也就是getAndIncrement方法中的current。

(3)要修改的新值B,也就是getAndIncrement方法中的next。

4.2 例子

CAS機制中,更新一個變量的時候,只有當變量的預期值A和內存地址V當中的實際值相同時,纔會將內存地址V對應的值修改爲B。下面我們來看一個具體的例子:

(1)在內存地址V當中,存儲着值爲10的變量。

(2)此時線程1想要把變量的值增加1。對線程1來說,舊的預期值A=10,要修改的新值B=11。

(3)但是,在線程1要提交更新之前,另一個線程2搶先一步,把內存地址V中的變量值率先更新成了11。

(4)此時,線程1開始提交更新,首先進行A和地址V的實際值比較(Compare),發現A不等於V的實際值,提交失敗。

(5)線程1重新獲取內存地址V的當前值,並重新計算想要修改的新值。此時對線程1來說,A=11,B=12。這個重新嘗試的過程被稱爲自旋。

(6)這一次比較幸運,沒有其他線程改變地址V的值。線程1進行Compare,發現A和地址V的實際值是相等的。

(7)線程1進行替換,把地址V的值替換爲B,也就是12。

      對比Synchronized,我們可以發現,Synchronized屬於悲觀鎖,悲觀地認爲程序中的併發情況嚴重,所以嚴防死守。CAS屬於樂觀鎖,樂觀地認爲程序中的併發情況不那麼嚴重,所以讓線程不斷去嘗試更新。

4.3 缺點

(1)ABA問題

       如果V的初始值是A,在準備賦值的時候檢查到它仍然是A,那麼能說它沒有改變過嗎?也許V經歷了這樣一個過程:它先變成了B,又變成了A,使用CAS檢查時

以爲它沒變,其實卻已經改變過了。

(2)CPU開銷較大

     在併發量比較高的情況下,如果許多線程反覆嘗試更新某一個變量,卻又一直更新不成功,循環往復,會給CPU帶來很大的壓力。

(3)不能保證代碼塊的原子性

     CAS機制所保證的只是一個變量的原子性操作,而不能保證整個代碼塊的原子性。比如需要保證3個變量共同進行原子性的更新,就不得不使用Synchronized了。

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