【CAS之1】CAS操做原理

參考網頁

主要參考html

https://www.jianshu.com/p/fb6e91b013ccjava

http://zl198751.iteye.com/blog/1848575算法

https://www.xilidou.com/2018/02/01/java-cas/緩存

其餘參考安全

https://my.oschina.net/huangcongmin12/blog/692907多線程

 

http://www.cnblogs.com/dayhand/p/3713303.html併發

https://my.oschina.net/vshcxl/blog/795495app

http://www.javashuo.com/article/p-ckzisqmq-mv.htmljvm

https://mritd.me/2017/02/06/java-cas/函數

CAS是什麼

CAS: 全稱Compare and swap,字面意思:「比較並交換」,一個 CAS 涉及到如下操做:

假設內存中的原數據V,舊的預期值A,須要修改的新值B

  1. 比較 A 與 V 是否相等。(比較)
  2. 若是比較相等,將 B 寫入 V。(交換)
  3. 返回操做是否成功。

當多個線程同時對某個資源進行CAS操做,只能有一個線程操做成功,可是並不會阻塞其餘線程,其餘線程只會收到操做失敗的信號。可見 CAS 實際上是一個樂觀鎖。

爲什麼說當多個線程同時對某個資源進行CAS操做,只能有一個線程操做成功?多個線程訪問,只有一次能成功的進行CAS操做(由於只有一個線程luck thread能夠成功的捕捉到A和V相等的那一刻,而後其餘線程訪問時A和V確定已經不相等了)。

CAS算法原理的一個描述

CAS 算法大體原理是:在對變量進行計算以前(如 ++ 操做),首先讀取原變量值,稱爲 舊的預期值 A,而後在更新以前再獲取當前內存中的值,稱爲 當前內存值 V,若是 A==V 則說明變量從未被其餘線程修改過,此時將會寫入新值 B,若是 A!=V 則說明變量已經被其餘線程修改過,當前線程應當什麼也不作。

CAS和synchronized

CAS和synchronized的區別

CAS是樂觀鎖,synchronized(重量級鎖)是悲觀鎖。

Java技術發展史先設計出的synchronized,synchronized會產生阻塞問題,後來又發展出了CAS。

閱讀JDK的源碼可知,J.U.C包其實是創建在CAS操做基礎上的。ReentrantLock這些類的底層其實就採用的CAS操做。

CAS和synchronized沒有絕對的好壞,各有各自適用的場景

能夠用CAS在無鎖的狀況下實現原子操做,但要明確應用場合,很是簡單的操做且又不想引入鎖能夠考慮使用CAS操做,當想要非阻塞地完成某一操做也能夠考慮CAS。不推薦在複雜操做中引入CAS,會使程序可讀性變差,且難以測試,同時會出現ABA問題。

不想引入鎖或者想非阻塞的完成併發操做能夠考慮使用CAS操做。可是CAS操做的代碼可讀性差難測試,複雜操做最好不用引入CAS

★CAS實現原理:硬件層級的原子操做--AtomInteger爲例進行分析

AtomicInteger -> sum.misc.Unsafe

AtomicInteger核心代碼

public class AtomicInteger extends Number implements java.io.Serializable {
    // 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;
    public final int get() {
        return value;
    }
}

代碼分析

1.Unsafe,是CAS的核心類,因爲Java方法沒法直接訪問底層系統,須要經過本地(native)方法來訪問,Unsafe至關於一個後門,基於該類能夠直接操做特定內存的數據。

2.變量valueOffset,表示該變量值在內存中的偏移地址,由於Unsafe就是根據內存偏移地址獲取數據的。

3.變量value用volatile修飾,保證了多線程之間的內存可見性。

AtomicInteger如何實現併發下的累加操做

public final int getAndAdd(int delta) {
    return unsafe.getAndAddInt(this, valueOffset, delta);
}

上述代碼已經使用到了Unsafe的方法。

sum.misc.Unsafe -> native(JNI方法)

unsafe.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;
}

代碼分析

假設線程A和線程B同時執行getAndAdd操做(分別跑在不一樣CPU上):

  1. AtomicInteger裏面的value原始值爲3,即主內存中AtomicInteger的value爲3,根據Java內存模型,線程A和線程B各自持有一份value的副本,值爲3。
  2. 線程A經過getIntVolatile(var1, var2)拿到value值3,這時線程A被掛起。
  3. 線程B也經過getIntVolatile(var1, var2)方法獲取到value值3,運氣好,線程B沒有被掛起,並執行compareAndSwapInt方法比較內存值也爲3,成功修改內存值爲2。
  4. 這時線程A恢復,執行compareAndSwapInt方法比較,發現本身手裏的值(3)和內存的值(2)不一致,說明該值已經被其它線程提早修改過了,那隻能從新來一遍了。
  5. 從新獲取value值,由於變量value被volatile修飾,因此其它線程對它的修改,線程A老是可以看到,線程A繼續執行compareAndSwapInt進行比較替換,直到成功。

整個過程當中,利用CAS保證了對於value的修改的併發安全,繼續深刻看看Unsafe類中的compareAndSwapInt方法實現。

public final native boolean compareAndSwapInt(Object paramObject, long paramLong, int paramInt1, int paramInt2);

Unsafe類中的compareAndSwapInt,是一個本地方法,該方法的實現位於unsafe.cpp中。

native(JNI方法)-> unsafe.cpp

unsafe.cpp

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

代碼分析

先想辦法拿到變量value在內存中的地址。

經過Atomic::cmpxchg實現比較替換,其中參數x是即將更新的值,參數e是原內存的值。

unsafe.cpp -> Atomic::cmpxchg

若是是Linux的x86,Atomic::cmpxchg方法的實現以下

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__表示彙編的開始
volatile表示禁止編譯器優化
LOCK_IF_MP是個內聯函數

#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "

Window的x86實現以下

inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
    int mp = os::isMP(); //判斷是不是多處理器
    _asm {
        mov edx, dest
        mov ecx, exchange_value
        mov eax, compare_value
        LOCK_IF_MP(mp)
        cmpxchg dword ptr [edx], ecx
    }
}
// Adding a lock prefix to an instruction on MP machine
// VC++ doesn't like the lock prefix to be on a single line
// so we can't insert a label after the lock prefix.
// By emitting a lock prefix, we can define a label after it.
#define LOCK_IF_MP(mp) __asm cmp mp, 0 \
                       __asm je L0 \
                       __asm _emit 0xF0 \
                       __asm L0:

代碼分析

LOCK_IF_MP根據當前系統是否爲多核處理器決定是否爲cmpxchg指令添加lock前綴。

1.若是是多處理器,爲cmpxchg指令添加lock前綴。

2.反之,就省略lock前綴。(單處理器會不須要lock前綴提供的內存屏障效果)

Atomic::cmpxchg -> lock前綴

intel手冊對lock前綴的說明以下:

1.確保後續指令執行的原子性。

在Pentium及以前的處理器中,帶有lock前綴的指令在執行期間會鎖住總線,使得其它處理器暫時沒法經過總線訪問內存,很顯然,這個開銷很大。在新的處理器中,Intel使用緩存鎖定來保證指令執行的原子性,緩存鎖定將大大下降lock前綴指令的執行開銷。

2.禁止該指令與前面和後面的讀寫指令重排序。

3.把寫緩衝區的全部數據刷新到內存中。

上面的第2點和第3點所具備的內存屏障效果,保證了CAS同時具備volatile讀和volatile寫的內存語義。

總結:AtomicInteger -> sum.misc.Unsafe -> native(JNI方法)-> unsafe.cpp -> Atomic::cmpxchg -> lock前綴

★總結一下 JAVA 的 cas 是怎麼實現的

    java 的 cas 利用的的是 unsafe 這個類提供的 cas 操做。

    unsafe 的cas 依賴了的是 jvm 針對不一樣的操做系統實現的 Atomic::cmpxchg

    Atomic::cmpxchg 的實現使用了彙編的 cas 操做,並使用 cpu 硬件提供的 lock信號保證其原子性

CAS的缺點

  1. ABA
  2. 循環時間長開銷大
  3. 只能保證一個共享變量的原子操做

JDK9 改變

相關文章
相關標籤/搜索