Java CAS 原理剖析

卡巴拉的樹發表於2018-02-02

Java CAS 原理剖析
在Java併發中,我們最初接觸的應該就是synchronized關鍵字了,但是synchronized屬於重量級鎖,很多時候會引起效能問題,volatile也是個不錯的選擇,但是volatile不能保證原子性,只能在某些場合下使用。

synchronized這種獨佔鎖屬於悲觀鎖,它是在假設一定會發生衝突的,那麼加鎖恰好有用,除此之外,還有樂觀鎖,樂觀鎖的含義就是假設沒有發生衝突,那麼我正好可以進行某項操作,如果要是發生衝突呢,那我就重試直到成功,樂觀鎖最常見的就是CAS

我們在讀Concurrent包下的類的原始碼時,發現無論是ReenterLock內部的AQS,還是各種Atomic開頭的原子類,內部都應用到了CAS,最常見的就是我們在併發程式設計時遇到的i++這種情況。傳統的方法肯定是在方法上加上synchronized關鍵字:

public class Test {

    public volatile int i;

    public synchronized void add() {
        i++;
    }
}
複製程式碼

但是這種方法在效能上可能會差一點,我們還可以使用AtomicInteger,就可以保證i原子的++了。

public class Test {

    public AtomicInteger i;

    public void add() {
        i.getAndIncrement();
    }
}
複製程式碼

我們來看getAndIncrement的內部:

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}
複製程式碼

再深入到getAndAddInt():

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}
複製程式碼

這裡我們見到compareAndSwapInt這個函式,它也是CAS縮寫的由來。那麼仔細分析下這個函式做了什麼呢?

首先我們發現compareAndSwapInt前面的this,那麼它屬於哪個類呢,我們看上一步getAndAddInt,前面是unsafe。這裡我們進入的Unsafe類。這裡要對Unsafe類做個說明。結合AtomicInteger的定義來說:

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;
    ...
複製程式碼

AtomicInteger資料定義的部分,我們可以看到,其實實際儲存的值是放在value中的,除此之外我們還獲取了unsafe例項,並且定義了valueOffset。再看到static塊,懂類載入過程的都知道,static塊的載入發生於類載入的時候,是最先初始化的,這時候我們呼叫unsafeobjectFieldOffsetAtomic類檔案中獲取value的偏移量,那麼valueOffset其實就是記錄value的偏移量的。

再回到上面一個函式getAndAddInt,我們看var5獲取的是什麼,通過呼叫unsafegetIntVolatile(var1, var2),這是個native方法,具體實現到JDK原始碼裡去看了,其實就是獲取var1中,var2偏移量處的值。var1就是AtomicIntegervar2就是我們前面提到的valueOffset,這樣我們就從記憶體裡獲取到現在valueOffset處的值了。

現在重點來了,compareAndSwapInt(var1, var2, var5, var5 + var4)其實換成compareAndSwapInt(obj, offset, expect, update)比較清楚,意思就是如果obj內的valueexpect相等,就證明沒有其他執行緒改變過這個變數,那麼就更新它為update,如果這一步的CAS沒有成功,那就採用自旋的方式繼續進行CAS操作,取出乍一看這也是兩個步驟了啊,其實在JNI裡是藉助於一個CPU指令完成的。所以還是原子操作。

CAS底層原理

CAS底層使用JNI呼叫C程式碼實現的,如果你有Hotspot原始碼,那麼在Unsafe.cpp裡可以找到它的實現:

static JNINativeMethod methods_15[] = {
    //省略一堆程式碼...
    {CC"compareAndSwapInt",  CC"("OBJ"J""I""I"")Z",      FN_PTR(Unsafe_CompareAndSwapInt)},
    {CC"compareAndSwapLong", CC"("OBJ"J""J""J"")Z",      FN_PTR(Unsafe_CompareAndSwapLong)},
    //省略一堆程式碼...
};
複製程式碼

我們可以看到compareAndSwapInt實現是在Unsafe_CompareAndSwapInt裡面,再深入到Unsafe_CompareAndSwapInt:

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
複製程式碼

p是取出的物件,addr是p中offset處的地址,最後呼叫了Atomic::cmpxchg(x, addr, e), 其中引數x是即將更新的值,引數e是原記憶體的值。程式碼中能看到cmpxchg有基於各個平臺的實現,這裡我選擇Linux X86平臺下的原始碼分析:

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  int mp = os::is_MP();
  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory");
  return exchange_value;
}
複製程式碼

這是一段小彙編,__asm__說明是ASM彙編,__volatile__禁止編譯器優化

// Adding a lock prefix to an instruction on MP machine
#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "
複製程式碼

os::is_MP判斷當前系統是否為多核系統,如果是就給匯流排加鎖,所以同一晶片上的其他處理器就暫時不能通過匯流排訪問記憶體,保證了該指令在多處理器環境下的原子性。

在正式解讀這段彙編前,我們來了解下嵌入彙編的基本格式:

asm ( assembler template
    : output operands                  /* optional */
    : input operands                   /* optional */
    : list of clobbered registers      /* optional */
    );
複製程式碼
  • template就是cmpxchgl %1,(%3)表示彙編模板

  • output operands表示輸出運算元,=a對應eax暫存器

  • input operand 表示輸入引數,%1 就是exchange_value, %3dest, %4就是mpr表示任意暫存器,a還是eax暫存器

  • list of clobbered registers就是些額外引數,cc表示編譯器cmpxchgl的執行將影響到標誌暫存器, memory告訴編譯器要重新從記憶體中讀取變數的最新值,這點實現了volatile的感覺。

那麼表示式其實就是cmpxchgl exchange_value ,dest,我們會發現%2也就是compare_value沒有用上,這裡就要分析cmpxchgl的語義了。cmpxchgl末尾l表示運算元長度為4,上面已經知道了。cmpxchgl會預設比較eax暫存器的值即compare_valueexchange_value的值,如果相等,就把dest的值賦值給exchange_value,否則,將exchange_value賦值給eax。具體彙編指令可以檢視Intel手冊CMPXCHG

最終,JDK通過CPU的cmpxchgl指令的支援,實現AtomicIntegerCAS操作的原子性。

CAS 的問題

  1. ABA問題

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

  1. 迴圈時間長開銷大

上面我們說過如果CAS不成功,則會原地自旋,如果長時間自旋會給CPU帶來非常大的執行開銷。

相關文章