java併發編程CAS機制原理分析

學習Java併發編程,CAS機制都是一個不得不掌握的知識點。這篇文章主要是從出現的緣由再到原理進行一個解析。但願對你有所幫助。編程

1、爲何須要CAS機制?

爲何須要CAS機制呢?咱們先從一個錯誤現象談起。咱們常用volatile關鍵字修飾某一個變量,代表這個變量是全局共享的一個變量,同時具備了可見性和有序性。可是卻沒有原子性。好比說一個常見的操做a++。這個操做其實能夠細分紅三個步驟:安全

(1)從內存中讀取a多線程

(2)對a進行加1操做併發

(3)將a的值從新寫入內存中app

在單線程狀態下這個操做沒有一點問題,可是在多線程中就會出現各類各樣的問題了。由於可能一個線程對a進行了加1操做,還沒來得及寫入內存,其餘的線程就讀取了舊值。形成了線程的不安全現象。如何去解決這個問題呢?最多見的方式就是使用AtomicInteger來修飾a。咱們能夠看一下代碼:ide

public class Test3 {
    //使用AtomicInteger定義a
    static AtomicInteger a = new AtomicInteger();
    public static void main(String[] args) {
        Test3 test = new Test3();
        Thread[] threads = new Thread[5];
        for (int i = 0; i < 5; i++) {
            threads[i] = new Thread(() -> {
                try {
                    for (int j = 0; j < 10; j++) {
                        //使用getAndIncrement函數進行自增操做
                        System.out.println(a.incrementAndGet());        
                        Thread.sleep(500);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
            threads[i].start();
        }
    }
}

如今咱們使用AtomicInteger類而且調用了incrementAndGet方法來對a進行自增操做。這個incrementAndGet是如何實現的呢?咱們能夠看一下AtomicInteger的源碼。函數

    /**
     * Atomically increments by one the current value.
     * @return the updated value
     */

    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

咱們到這一步能夠看到其實就是usafe調用了getAndAddInt的方法實現的,可是如今咱們還看不出什麼,咱們再深刻到源碼中看看getAndAddInt方法又是如何實現的,oop

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方法,這個compareAndSwapInt方法其實就是CAS機制。所以若是咱們想搞清楚AtomicInteger的原子操做是如何實現的,咱們就必需要把CAS機制搞清楚,這也是爲何咱們須要掌握CAS機制的緣由。性能

2、分析CAS

一、基本含義學習

CAS全拼又叫作compareAndSwap,從名字上的意思就知道是比較交換的意思。比較交換什麼呢?

過程是這樣:它包含 3 個參數 CAS(V,E,N),V表示要更新變量的值,E表示預期值,N表示新值。僅當 V值等於E值時,纔會將V的值設爲N,若是V值和E值不一樣,則說明已經有其餘線程作兩個更新,則當前線程則什麼都不作。最後,CAS 返回當前V的真實值。

咱們舉一個我以前舉過的例子來講明這個過程:

好比說給你兒子訂婚。你兒子就是內存位置,你本來覺得你兒子是和楊貴妃在一塊兒了,結果在訂婚的時候發現兒子身邊是西施。這時候該怎麼辦呢?你一氣之下不作任何操做。若是兒子身邊是你預想的楊貴妃,你一看很開心就給他們訂婚了,也叫做執行操做。如今你應該明白了吧。

CAS 操做時抱着樂觀的態度進行的,它老是認爲本身能夠成功完成操做。因此CAS也叫做樂觀鎖,那什麼是悲觀鎖呢?悲觀鎖就是咱們以前赫赫有名的synchronized。悲觀鎖的思想你能夠這樣理解,一個線程想要去得到這個鎖可是卻獲取不到,必需要別人釋放了才能夠。

二、底層原理

想要弄清楚其底層原理,深刻到源碼是最好的方式,在上面咱們已經經過源碼看到了其實就是Usafe的方法來完成的,在這個方法中使用了compareAndSwapInt這個CAS機制。所以,如今咱們有必要進一步深刻進去看看:

public final class Unsafe {
    // compareAndSwapInt 是 native 類型的方法
    public final native boolean compareAndSwapInt(
        Object o, 
        long offset,
        int expected,
        int x
    )
;
    //剩餘還有不少方法
}

咱們能夠看到這裏面主要有四個參數,第一個參數就是咱們操做的對象a,第二個參數是對象a的地址偏移量,第三個參數表示咱們期待這個a是什麼值,第四個參數表示的是a的實際值。

不過這裏咱們會發現這個compareAndSwapInt是一個native方法,也就是說再往下走就是C語言代碼,若是咱們保持好奇心,能夠繼續深刻進去看看。

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);
  // 根據偏移量valueOffset,計算 value 的地址
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  // 調用 Atomic 中的函數 cmpxchg來進行比較交換
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

上面的代碼咱們解讀一下:首先使用jint計算了value的地址,而後根據這個地址,使用了Atomic的cmpxchg方法進行比較交換。如今問題又拋給了這個cmpxchg,真實實現的是這個函數。咱們再進一步深刻看看,真相已經離咱們不遠了。

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");
  /*
   * 根據操做系統類型調用不一樣平臺下的重載函數,
     這個在預編譯期間編譯器會決定調用哪一個平臺下的重載函數
  */

    return (unsigned int)Atomic::cmpxchg((jint)exchange_value, 
                     (volatile jint*)dest, (jint)compare_value);
}

皮球又一次被完美的踢走了,如今在不一樣的操做系統下會調用不一樣的cmpxchg重載函數,我如今用的是win10系統,因此咱們看看這個平臺下的實現,彆着急再往下走走:

inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, 
                            jint compare_value) {
  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
  }
}

這塊的代碼就有點涉及到彙編指令相關的代碼了,到這一步就完全接近真相了,首先三個move指令表示的是將後面的值移動到前面的寄存器上。而後調用了LOCK_IF_MP和下面cmpxchg彙編指令進行了比較交換。如今咱們不知道這個LOCK_IF_MP和cmpxchg是如何交換的,不要緊咱們最後再深刻一下。

真相來了,他來了,他真的來了。

inline jint Atomic::cmpxchg (jint exchange_value, 
                             volatile jint* dest, jint compare_value) {
  //一、 判斷是不是多核 CPU
  int mp = os::is_MP();
  __asm {
    //二、 將參數值放入寄存器中
    mov edx, dest   
    mov ecx, exchange_value
    mov eax, compare_value 
    //三、LOCK_IF_MP指令
    cmp mp, 0
    //四、 若是 mp = 0,代表線程運行在單核CPU環境下。此時 je 會跳轉到 L0 標記處,直接執行 cmpxchg 指令
    je L0
    _emit 0xF0
//五、這裏真正實現了比較交換
L0:
    /*
     * 比較並交換。簡單解釋一下下面這條指令,熟悉彙編的朋友能夠略過下面的解釋:
     *   cmpxchg: 即「比較並交換」指令
     *   dword: 全稱是 double word 表示兩個字,一共四個字節
     *   ptr: 全稱是 pointer,與前面的 dword 連起來使用,代表訪問的內存單元是一個雙字單元 
     * 這一條指令的意思就是:
             將 eax 寄存器中的值(compare_value)與 [edx] 雙字內存單元中的值進行對比,
             若是相同,則將 ecx 寄存器中的值(exchange_value)存入 [edx] 內存單元中。
     */

    cmpxchg dword ptr [edx], ecx
  }
}

到這一步了,相信你應該理解了這個CAS真正實現的機制了吧,最終是由操做系統的彙編指令完成的。

三、CAS機制的優缺點

(1)優勢

一開始在文中咱們曾經提到過,cas是一種樂觀鎖,並且是一種非阻塞的輕量級的樂觀鎖,什麼是非阻塞式的呢?其實就是一個線程想要得到鎖,對方會給一個迴應表示這個鎖能不能得到。在資源競爭不激烈的狀況下性能高,相比synchronized重量鎖,synchronized會進行比較複雜的加鎖,解鎖和喚醒操做。

(2)缺點

缺點也是一個很是重要的知識點,由於涉及到了一個很是著名的問題,叫作ABA問題。假設一個變量 A ,修改成 B以後又修改成 A,CAS 的機制是沒法察覺的,但實際上已經被修改過了。這就是ABA問題,

ABA問題會帶來大量的問題,好比說數據不一致的問題等等。咱們能夠舉一個例子來解釋說明。

你有一瓶水放在桌子上,別人把這瓶水喝完了,而後從新倒上去。你再去喝的時候發現水仍是跟以前同樣,就誤覺得是剛剛那杯水。若是你知道了真相,那是別人用過了你還會再用嘛?舉一個比較黃一點的例子,女友被別人睡過以後又還回來,仍是以前的那個女友嘛?

ABA能夠有不少種方式來解決,咱們在後續的文章中再進行敘述和討論。

相關文章
相關標籤/搜索