Java CAS 原理剖析

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

synchronized這種獨佔鎖屬於悲觀鎖,它是在假設必定會發生衝突的,那麼加鎖剛好有用,除此以外,還有樂觀鎖,樂觀鎖的含義就是假設沒有發生衝突,那麼我正好能夠進行某項操做,若是要是發生衝突呢,那我就重試直到成功,樂觀鎖最多見的就是CASjava

咱們在讀Concurrent包下的類的源碼時,發現不管是ReenterLock內部的AQS,仍是各類Atomic開頭的原子類,內部都應用到了CAS,最多見的就是咱們在併發編程時遇到的i++這種狀況。傳統的方法確定是在方法上加上synchronized關鍵字:編程

public class Test {

    public volatile int i;

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

可是這種方法在性能上可能會差一點,咱們還能夠使用AtomicInteger,就能夠保證i原子的++了。bash

public class Test {

    public AtomicInteger i;

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

咱們來看getAndIncrement的內部:併發

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

再深刻到getAndAddInt():app

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的定義來講:oop

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帶來很是大的執行開銷。

相關文章
相關標籤/搜索