面試:Java併發之CAS和Atomic詳細解讀

點擊上方 Java後端技術棧選擇 設爲星標

回覆面試,獲取乾貨

來源:http://8nn.co/a4Qc

無鎖

“無鎖”即不用鎖保證程序的併發。無鎖採用CAS(compare and swap)算法來處理線程衝突。

CAS

CAS的全稱爲Compare-And-Swap,直譯就是對比交換。是一條CPU的原子指令,其作用是讓CPU先進行比較兩個值是否相等,然後原子地更新某個位置的值,經過調查發現,其實現方式是基於硬件平臺的彙編指令,就是說CAS是靠硬件實現的,JVM只是封裝了彙編調用,那些AtomicInteger類便是使用了這些封裝後的接口。

CAS包含3個參數CAS(V,E,N)。V表示要更新的變量, E表示預期值, N表示新值。僅當V值等於E值時, 纔會將V的值設爲N, 如果V值和E值不同, 則說明已經有其他線程做了更新, 則當前線程什麼都不做。最後, CAS返回當前V的真實值。CAS操作是抱着樂觀的態度進行的, 它總是認爲自己可以成功完成操作。當多個線程同時使用CAS操作一個變量時, 只有一個會勝出, 併成功更新,其餘均會失敗。失敗的線程不會被掛起,僅是被告知失敗, 並且允許再次嘗試,當然也允許失敗的線程放棄操作。基於這樣的原理, CAS操作即時沒有鎖,也可以發現其他線程對當前線程的干擾, 並進行恰當的處理。

CAS操作是原子性的,所以多線程併發使用CAS更新數據時,可以不使用鎖。JDK中大量使用了CAS來更新數據而防止加鎖(synchronized 重量級鎖)來保持原子更新。

java.util.concurrent包都中的實現類都是基於volatile和CAS來實現的。尤其java.util.concurrent.atomic包下的原子類。

簡單介紹下volatile特性(具體可以參見另一篇筆記"Java併發之volatile"):

  • 內存可見性(當一個線程修改volatile變量的值時,另一個線程就可以實時看到此變量的更新值)

  • 禁止指令重排(volatile變量之前的變量執行先於volatile變量執行,volatile之後的變量執行在volatile變量之後)

AtomicInteger 源碼解析

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;
    static {
        try {
            //用於獲取value字段相對當前對象的“起始地址”的偏移量
            valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    private volatile int value;
    //返回當前值
    public final int get() {
        return value;
    }
    //遞增加detla
    public final int getAndAdd(int delta) {
        //三個參數,1、當前的實例 2、value實例變量的偏移量 3、當前value要加上的數(value+delta)。
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }
    //遞增加1
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
...
}

可以看到 AtomicInteger 底層用的是volatile的變量和CAS來進行更改數據的。volatile保證線程的可見性,多線程併發時,一個線程修改數據,可以保證其它線程立馬看到修改後的值

CAS 保證數據更新的原子性。

Unsafe源碼解析

Unsafe 類中代碼反編譯出來的結果如下:

public final int getAndAddInt(Object paramObject, long paramLong, int paramInt)
  {
    int i;
    do
      i = getIntVolatile(paramObject, paramLong);
    while (!compareAndSwapInt(paramObject, paramLong, i, i + paramInt));
    return i;
  }
  public final long getAndAddLong(Object paramObject, long paramLong1, long paramLong2)
  {
    long l;
    do
      l = getLongVolatile(paramObject, paramLong1);
    while (!compareAndSwapLong(paramObject, paramLong1, l, l + paramLong2));
    return l;
  }
  public final int getAndSetInt(Object paramObject, long paramLong, int paramInt)
  {
    int i;
    do
      i = getIntVolatile(paramObject, paramLong);
    while (!compareAndSwapInt(paramObject, paramLong, i, paramInt));
    return i;
  }
  public final long getAndSetLong(Object paramObject, long paramLong1, long paramLong2)
  {
    long l;
    do
      l = getLongVolatile(paramObject, paramLong1);
    while (!compareAndSwapLong(paramObject, paramLong1, l, paramLong2));
    return l;
  }
  public final Object getAndSetObject(Object paramObject1, long paramLong, Object paramObject2)
  {
    Object localObject;
    do
      localObject = getObjectVolatile(paramObject1, paramLong);
    while (!compareAndSwapObject(paramObject1, paramLong, localObject, paramObject2));
    return localObject;
  }

從源碼中發現,內部使用自旋的方式進行CAS更新(while循環進行CAS更新,如果更新失敗,則循環再次重試)。

又從Unsafe類中發現,原子操作其實只支持下面三個方法:

public final native boolean compareAndSwapObject(Object paramObject1, long paramLong, Object paramObject2, Object paramObject3);
public final native boolean compareAndSwapInt(Object paramObject, long paramLong, int paramInt1, int paramInt2);
public final native boolean compareAndSwapLong(Object paramObject, long paramLong1, long paramLong2, long paramLong3);

Unsafe只提供了3種CAS方法:compareAndSwapObject、compareAndSwapInt和compareAndSwapLong。都是native方法。

native方法中使用了CPU原子指令:cmpxchg。它的作用是“比較並交換”。

如:CMPXCHG r/m,r 將累加器AL/AX/EAX/RAX中的值與首操作數(目的操作數)比較,如果相等,第2操作數(源操作數)的值裝載到首操作數,zf置1。如果不等, 首操作數的值裝載到AL/AX/EAX/RAX並將zf清0 該指令只能用於486及其後繼機型。第2操作數(源操作數)只能用8位、16位或32位寄存器。第1操作數(目地操作數)則可用寄存器或任一種存儲器尋址方式。

AtomicBoolean 源碼解析

public class AtomicBoolean implements java.io.Serializable {
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;
    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicBoolean.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    private volatile int value;
    public AtomicBoolean(boolean initialValue) {
        value = initialValue ? 1 : 0;
    }
    public final boolean compareAndSet(boolean expect, boolean update) {
        int e = expect ? 1 : 0;
        int u = update ? 1 : 0;
        return unsafe.compareAndSwapInt(this, valueOffset, e, u);
    }
    ...
}

從AtomicBoolean源碼,發現他底層也是使用volatile類型的int 變量,跟AtomicInteger 實現方式一樣,只不過是把Boolean轉換成 0和1進行操作。

所以原子更新char、float和double變量也可以轉換成int 或long來實現CAS的操作。

CAS缺點

  • ABA問題。因爲CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加一,那麼A-B-A 就會變成1A-2B-3A。從Java1.5開始JDK的atomic包裏提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法作用是首先檢查當前引用是否等於預期引用,並且當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值。

  • 循環時間長開銷大。自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷。

總結

Atomic使用volatile保證可見性和有序性,使用unsafe類下面的CAS方法保證原子性。

推薦閱讀

分佈式鎖的三種實現

淺談MySQL自增鎖

InnoDB中的意向鎖,不與行級鎖衝突的表級鎖

面試被問ReentrantLock的公平鎖與非公平鎖

面試題【synchronized 的實現原理以及鎖優化】

目前10000+ 人已關注加入我們

       

        

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