CAS原始碼分析

搜雲庫技術團隊發表於2019-03-04

CAS的全稱為Compare And Swap,直譯就是比較交換。是一條CPU的原子指令,其作用是讓CPU先進行比較兩個值是否相等,然後原子地更新某個位置的值,其實現方式是基於硬體平臺的彙編指令,在intel的CPU中,使用的是cmpxchg指令,就是說CAS是靠硬體實現的,從而在硬體層面提升效率。

CSA 原理

利用CPU的CAS指令,同時藉助JNI來完成Java的非阻塞演算法,其它原子操作都是利用類似的特性完成的。 在 java.util.concurrent 下面的原始碼中,Atomic, ReentrantLock 都使用了Unsafe類中的方法來保證併發的安全性。

CAS操作是原子性的,所以多執行緒併發使用CAS更新資料時,可以不使用鎖,JDK中大量使用了CAS來更新資料而防止加鎖來保持原子更新。

CAS 操作包含三個運算元 :記憶體偏移量位置(V)、預期原值(A)和新值(B)。 如果記憶體位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新為新值 。否則,處理器不做任何操作。

原始碼分析

下面來看一下 java.util.concurrent.atomic.AtomicInteger.javagetAndIncrement()getAndDecrement()是如何利用CAS實現原子性操作的。

AtomicInteger 原始碼解析

// 使用 unsafe 類的原子操作方式
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); }
}
複製程式碼

valueOffset 欄位表示"value" 記憶體位置,在compareAndSwap 方法 ,第二個引數會用到.

關於偏移量

CAS原始碼分析

Unsafe 呼叫C 語言可以通過偏移量對變數進行操作

//volatile變數value
private volatile int value;

 /**
 * 建立具有給定初始值的新 AtomicInteger
 *
 * @param initialValue 初始值
 */
public AtomicInteger(int initialValue) {
    value = initialValue;
}

//返回當前的值
public final int get() {
    return value;
}
//原子更新為新值並返回舊值
public final int getAndSet(int newValue) {
    return unsafe.getAndSetInt(this, valueOffset, newValue);
}
//最終會設定成新值
public final void lazySet(int newValue) {
    unsafe.putOrderedInt(this, valueOffset, newValue);
}
//如果輸入的值等於預期值,則以原子方式更新為新值
public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
複製程式碼
//方法相當於原子性的 ++i
public final int getAndIncrement() {
    //三個引數,1、當前的例項 2、value例項變數的偏移量 3、遞增的值。
    return unsafe.getAndAddInt(this, valueOffset, 1);
}
//方法相當於原子性的 --i
public final int getAndDecrement() {
    //三個引數,1、當前的例項 2、value例項變數的偏移量 3、遞減的值。
    return unsafe.getAndAddInt(this, valueOffset, -1);
}
複製程式碼

實現邏輯封裝在 Unsafe 中 getAndAddInt 方法,繼續往下看,Unsafe 原始碼解析

Unsafe 原始碼解析

在JDK8中追蹤可見sun.misc.Unsafe這個類是無法看見原始碼的,開啟openjdk8原始碼看

檔案:openjdk-8-src-b132-03_mar_2014.zip

目錄:openjdk\jdk\src\share\classes\sun\misc\Unsafe.java

通常我們最好也不要使用Unsafe類,除非有明確的目的,並且也要對它有深入的瞭解才行。要想使用Unsafe類需要用一些比較tricky的辦法。Unsafe類使用了單例模式,需要通過一個靜態方法getUnsafe()來獲取。但Unsafe類做了限制,如果是普通的呼叫的話,它會丟擲一個SecurityException異常;只有由主類載入器載入的類才能呼叫這個方法。

下面是sun.misc.Unsafe.java類原始碼


//獲取Unsafe例項靜態方法
@CallerSensitive
public static Unsafe getUnsafe() {
    Class<?> caller = Reflection.getCallerClass();
    if (!VM.isSystemDomainLoader(caller.getClassLoader()))
        throw new SecurityException("Unsafe");
    return theUnsafe;
}

複製程式碼

網上也有一些辦法來用主類載入器載入使用者程式碼,最簡單方法是利用Java反射,方法如下:

private static Unsafe unsafe;

static {
    try {
        //通過反射獲取rt.jar下的Unsafe類
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        unsafe = (Unsafe) field.get(null);
    } catch (Exception e) {
        System.out.println("Get Unsafe instance occur error" + e);
    }
}
複製程式碼

獲取到Unsafe例項之後,我們就可以為所欲為了。Unsafe類提供了以下這些功能:

www.cnblogs.com/pkufork/p/j…

    //native硬體級別的原子操作
    //類似的有compareAndSwapInt,compareAndSwapLong,compareAndSwapBoolean,compareAndSwapChar等等。
    public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);

    //內部使用自旋的方式進行CAS更新(while迴圈進行CAS更新,如果更新失敗,則迴圈再次重試)
    public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        do {
            //獲取物件記憶體地址偏移量上的數值v
            v = getIntVolatile(o, offset);
            //如果現在還是v,設定為 v + delta,否則返回false,繼續迴圈再次重試.
        } while (!compareAndSwapInt(o, offset, v, v + delta));
        return v;
    }
複製程式碼

利用 Unsafe 類的 JNI compareAndSwapInt 方法實現,使用CAS實現一個原子操作更新,

compareAndSwapInt 四個引數

1、當前的例項 2、例項變數的記憶體地址偏移量 3、預期的舊值 4、要更新的值

unsafe.cpp 深層次解析

// unsafe.cpp
/*
 * 這個看起來好像不像一個函式,不過不用擔心,不是重點。UNSAFE_ENTRY 和 UNSAFE_END 都是巨集,
 * 在預編譯期間會被替換成真正的程式碼。下面的 jboolean、jlong 和 jint 等是一些型別定義(typedef):
 *
 * 省略部分內容
 */
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);
  // 根據偏移量,計算 value 的地址。這裡的 offset 就是 AtomaicInteger 中的 valueOffset
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  // 呼叫 Atomic 中的函式 cmpxchg,該函式宣告於 Atomic.hpp 中
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

// atomic.cpp
unsigned Atomic::cmpxchg(unsigned int exchange_value, volatile unsigned int* dest, unsigned int compare_value) {
  assert(sizeof(unsigned int) == sizeof(jint), "more work to do");
  /*
   * 根據作業系統型別呼叫不同平臺下的過載函式,這個在預編譯期間編譯器會決定呼叫哪個平臺下的過載
   * 函式。相關的預編譯邏輯如下:
   *
   * atomic.inline.hpp:
   *    #include "runtime/atomic.hpp"
   *  
   *    // Linux
   *    #ifdef TARGET_OS_ARCH_linux_x86
   *    # include "atomic_linux_x86.inline.hpp"
   *    #endif
   * 
   *    // 省略部分程式碼
   *  
   *    // Windows
   *    #ifdef TARGET_OS_ARCH_windows_x86
   *    # include "atomic_windows_x86.inline.hpp"
   *    #endif
   *  
   *    // BSD
   *    #ifdef TARGET_OS_ARCH_bsd_x86
   *    # include "atomic_bsd_x86.inline.hpp"
   *    #endif
   *
   * 接下來分析 atomic_windows_x86.inline.hpp 中的 cmpxchg 函式實現
   */
  return (unsigned int)Atomic::cmpxchg((jint)exchange_value, (volatile jint*)dest,
                                       (jint)compare_value);
}
複製程式碼

上面的分析看起來比較多,不過主流程並不複雜。如果不糾結於程式碼細節,還是比較容易看懂的。接下來,我會分析 Windows 平臺下的 Atomic::cmpxchg 函式。繼續往下看吧。

// atomic_windows_x86.inline.hpp
#define LOCK_IF_MP(mp) __asm cmp mp, 0  \
                       __asm je L0      \
                       __asm _emit 0xF0 \
                       __asm L0:
            
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
  // alternative for InterlockedCompareExchange
  int mp = os::is_MP();
  __asm {
    mov edx, dest
    mov ecx, exchange_value
    mov eax, compare_value
    LOCK_IF_MP(mp)
    cmpxchg dword ptr [edx], ecx
  }
}
複製程式碼

上面的程式碼由 LOCK_IF_MP 預編譯識別符號和 cmpxchg 函式組成。為了看到更清楚一些,我們將 cmpxchg 函式中的 LOCK_IF_MP 替換為實際內容。如下:

inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
  // 判斷是否是多核 CPU
  int mp = os::is_MP();
  __asm {
    // 將引數值放入暫存器中
    mov edx, dest    // 注意: dest 是指標型別,這裡是把記憶體地址存入 edx 暫存器中
    mov ecx, exchange_value
    mov eax, compare_value
  
    // LOCK_IF_MP
    cmp mp, 0
    /*
     * 如果 mp = 0,表明是執行緒執行在單核 CPU 環境下。此時 je 會跳轉到 L0 標記處,
     * 也就是越過 _emit 0xF0 指令,直接執行 cmpxchg 指令。也就是不在下面的 cmpxchg 指令
     * 前加 lock 字首。
     */
    je L0
    /*
     * 0xF0 是 lock 字首的機器碼,這裡沒有使用 lock,而是直接使用了機器碼的形式。至於這樣做的
     * 原因可以參考知乎的一個回答:
     *     https://www.zhihu.com/question/50878124/answer/123099923
     */
    _emit 0xF0
L0:
    /*
     * 比較並交換。簡單解釋一下下面這條指令,熟悉彙編的朋友可以略過下面的解釋:
     *   cmpxchg: 即“比較並交換”指令
     *   dword: 全稱是 double word,在 x86/x64 體系中,一個
     *          word = 2 byte,dword = 4 byte = 32 bit
     *   ptr: 全稱是 pointer,與前面的 dword 連起來使用,表明訪問的記憶體單元是一個雙字單元
     *   [edx]: [...] 表示一個記憶體單元,edx 是暫存器,dest 指標值存放在 edx 中。
     *          那麼 [edx] 表示記憶體地址為 dest 的記憶體單元
     *        
     * 這一條指令的意思就是,將 eax 暫存器中的值(compare_value)與 [edx] 雙字記憶體單元中的值
     * 進行對比,如果相同,則將 ecx 暫存器中的值(exchange_value)存入 [edx] 記憶體單元中。
     */
    cmpxchg dword ptr [edx], ecx
  }
}
複製程式碼

到這裡 CAS 的實現過程就講了,CAS 的實現離不開處理器的支援。以上這麼多程式碼,其實核心程式碼就是一條帶 lock 字首的 cmpxchg 指令,即lock cmpxchg dword ptr [edx], ecx

通過上述的分析,可以發現AtomicInteger原子類的內部幾乎是基於前面分析過Unsafe類中的CAS相關操作的方法實現的,這也同時證明AtomicInteger getAndIncrement自增操作實現過程,是基於無鎖實現的。

CAS的ABA問題及其解決方案

假設這樣一種場景,當第一個執行緒執行CAS(V,E,U)操作。在獲取到當前變數V,準備修改為新值U前,另外兩個執行緒已連續修改了兩次變數V的值,使得該值又恢復為舊值,這樣的話,我們就無法正確判斷這個變數是否已被修改過,如下圖:

CAS原始碼分析

這就是典型的CAS的ABA問題,一般情況這種情況發現的概率比較小,可能發生了也不會造成什麼問題,比如說我們對某個做加減法,不關心數字的過程,那麼發生ABA問題也沒啥關係。但是在某些情況下還是需要防止的,那麼該如何解決呢?在Java中解決ABA問題,我們可以使用以下原子類

AtomicStampedReference類

AtomicStampedReference原子類是一個帶有時間戳的物件引用,在每次修改後,AtomicStampedReference不僅會設定新值而且還會記錄更改的時間。當AtomicStampedReference設定物件值時,物件值以及時間戳都必須滿足期望值才能寫入成功,這也就解決了反覆讀寫時,無法預知值是否已被修改的窘境

底層實現為: 通過Pair私有內部類儲存資料和時間戳, 並構造volatile修飾的私有例項

接著看 java.util.concurrent.atomic.AtomicStampedReference類的compareAndSet()方法的實現:

private static class Pair<T> {
    final T reference;
    final int stamp;
  
    //最好不要重複的一個資料,決定資料是否能設定成功,時間戳會重複
    private Pair(T reference, int stamp) {
        this.reference = reference;
        this.stamp = stamp;
    }
    static <T> Pair<T> of(T reference, int stamp) {
        return new Pair<T>(reference, stamp);
    }
}
複製程式碼

同時對當前資料和當前時間進行比較,只有兩者都相等是才會執行casPair()方法,

單從該方法的名稱就可知是一個CAS方法,最終呼叫的還是Unsafe類中的compareAndSwapObject方法

到這我們就很清晰AtomicStampedReference的內部實現思想了,

通過一個鍵值對Pair儲存資料和時間戳,在更新時對資料和時間戳進行比較,

只有兩者都符合預期才會呼叫UnsafecompareAndSwapObject方法執行數值和時間戳替換,也就避免了ABA的問題。

/**
 * 原子更新帶有版本號的引用型別。
 * 該類將整數值與引用關聯起來,可用於原子的更資料和資料的版本號。
 * 可以解決使用CAS進行原子更新時,可能出現的ABA問題。
 */
public class AtomicStampedReference<V> {
    //靜態內部類Pair將對應的引用型別和版本號stamp作為它的成員
    private static class Pair<T> {
      
        //最好不要重複的一個資料,決定資料是否能設定成功,建議時間戳
        final T reference;
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
      
        //根據reference和stamp來生成一個Pair的例項
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }
  
    //作為一個整體的pair變數被volatile修飾
    private volatile Pair<V> pair;
 
    //構造方法,引數分別是初始引用變數的值和初始版本號
    public AtomicStampedReference(V initialRef, int initialStamp) {
        pair = Pair.of(initialRef, initialStamp);
    }
  
    ....
  
    private static final sun.misc.Unsafe UNSAFE = sun.misc.Unsafe.getUnsafe();
  
    private static final long pairOffset = objectFieldOffset(UNSAFE, "pair", AtomicStampedReference.class);
 
    //獲取pair成員的偏移地址
    static long objectFieldOffset(sun.misc.Unsafe UNSAFE,
                                  String field, Class<?> klazz) {
        try {
            return UNSAFE.objectFieldOffset(klazz.getDeclaredField(field));
        } catch (NoSuchFieldException e) {
            NoSuchFieldError error = new NoSuchFieldError(field);
            error.initCause(e);
            throw error;
        }
    }
}
複製程式碼


/**
 * @param 期望(老的)引用
 * @param       (新的)引用資料
 * @param 期望(老的)標誌stamp(時間戳)值
 * @param       (新的)標誌stamp(時間戳)值
 * @return 是否成功
 */
public boolean compareAndSet(V expectedReference,V   newReference,int expectedStamp,int newStamp) {
       
    Pair<V> current = pair;
    return
        // 期望(老的)引用 == 當前引用
        expectedReference == current.reference &&
        // 期望(老的)標誌stamp(時間戳)值 == 當前標誌stamp(時間戳)值
        expectedStamp == current.stamp &&
      
        // (新的)引用資料 == 當前引用資料 並且 (新的)標誌stamp(時間戳)值 ==當前標誌stamp(時間戳)值
        ((newReference == current.reference && newStamp == current.stamp) ||
          #原子更新值
         casPair(current, Pair.of(newReference, newStamp)));
       
}
 
 //當引用型別的值與期望的一致的時候,原子的更改版本號為新的值。該方法只修改版本號,不修改引用變數的值,成功返回true
public boolean attemptStamp(V expectedReference, int newStamp) {
    Pair<V> current = pair;
    return
        expectedReference == current.reference &&
        (newStamp == current.stamp ||
         casPair(current, Pair.of(expectedReference, newStamp)));
}

/**
 * CAS真正實現方法
 */
private boolean casPair(Pair<V> cmp, Pair<V> val) {
        return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}
複製程式碼

期望 Pair cmp(A) == 當前記憶體存偏移量位置 Pair(V),就更新值 Pair val(B)成功返回true 否則 false

public static void main(String[] args) {
    AtomicStampedReference<Integer> num = new AtomicStampedReference<Integer>(1, 0);

    Integer i = num.getReference();
    int stamped = num.getStamp();

    if (num.compareAndSet(i, i + 1, stamped, stamped + 1)) {
        System.out.println("測試成功");
    }
}
複製程式碼

通過以上原子更新方法,可見 AtomicStampedReference就是利用了Unsafe的CAS方法+Volatile關鍵字對儲存實際的引用變數和int的版本號的Pair例項進行更新。

參考: www.cnblogs.com/nullllun/p/…
blog.csdn.net/a67474506/a…

CAS原始碼分析

相關文章