CAS 是什麼

CAS (Compare And Swap) 比較並交換html

CAS 是一條CPU併發原語。功能是判斷內存某個位置的值是否爲預期值,若是是則更改成最新值,這個過程是原子的。CAS併發原語體如今Java語言中就是sun.misc.Unsafe類中的各個本地方法。這是一種徹底依賴於硬件的功能,經過它實現了原子操做。原語的執行時連續的,在執行過程當中不容許被中斷,也就是說CAS是一條CPU的原子指令,不會形成數據不一致。java

CAS的做用是比較當前工做內存中的值和主內存中的值,若是相同則執行規定操做,不然繼續比較直到主內存和工做內存中的值一致爲止。 若是單看這句話不知道在說什麼,那麼就繼續往下看,通篇結束以後再看讀一下這句話吧。安全

這一篇經過AtomicInteger中兩個方法來探索CAS究竟是如何實現的。多線程

AtomicInteger的兩個方法

compareAndSet

先來看三行代碼併發

AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5,10)+ "\t current data : " +atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5,9)+ "\t current data : " +atomicInteger.get()); 
複製代碼

上篇文章對volatile的理解 提到了,可使用原子類AtomicInteger 解決原子性問題。這裏用到了AtomicInteger.compareAndSet方法,兩個參數分別是指望值更新值,若是指望值與此時內存中的值相同,則將內存中的值更新爲更新值,方法返回值爲布爾類型,表示是否更新成功。以上三行代碼,初始化時設置內存中值爲5,第二行指望值與更新值分別爲5和10,若是指望值5與內存中的值5相同,則將內存中的值更新爲10,第二行返回true,當前內存中的值爲10;第三行指望值爲5,當前內存中的值爲10,不符合指望結果,則不更新內存中的結果,返回false,當前內存中的值未更改,仍是10。代碼輸出結果以下:post

true	 current data : 10
false	 current data : 10
複製代碼

概述一下,CAS涉及到3個操做數,內存值V,舊的預期值A,要修改的更新值B。當且僅當預期值A和內存值V相同時,將內存值V修改成B,不然什麼都不作。this

getAndIncrement

咱們知道在多線程狀況下i++操做其實包含了三個步驟,是線程不安全的。咱們可使用atomicInteger.getAndIncrement() 實現線程安全的i++,下面經過看下該方法的實現瞭解如何作到線程安全。atom

/** * Atomically increments by one the current value. * * @return the previous value */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
複製代碼

以上是getAndIncrement的源代碼,註釋在當前值的基礎上原子增長一。實現爲調用unsafe類的getAndAddInt方法,該方法有三個參數,分別是當前對象、內存偏移量、1,再往下看,就進入到unsafe類。spa

  1. unsafe類
    unsafe類是CAS的核心類。java是沒法直接訪問底層系統的,須要經過本地(native)方法來訪問,Unsafe的全部方法都是native來修飾的,也就是說Unsafe類中的基礎方法都直接調用操做系統底層資源執行相應任務,便可以像C的指針同樣直接訪問內存。
  2. 變量valueOffset,表示該變量值在內存中的偏移地址,由於Unsafe就是根據內存偏移地址獲取數據的。如下代碼給出了valueOffset的來源
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;
複製代碼
  1. 變量value用volatile修飾,保證了多線程之間內存可見性。所以拿到的內存地址老是最新的。

瞭解這幾個概念,咱們繼續往下看,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;
    }
複製代碼

這仍然是存在於unfase的方法,var1對應this,即當前對象,var2爲內存偏移量,var4便是要添加的「1」。
當前方法爲do-while結構,根據方法名中getAndAdd,首先get操做,do結構首先根據對象與內存地址獲取當前值var5,從物理主內存中拷貝到本身的線程工做內存;而後關鍵點來了:while裏面判斷若是這一時刻內存地址中的值與var5相同,則修改成var5+var4,即Add成功,這時返回true,while取反,條件不成立,再也不執行do;若是剛拿到var5的值,主物理內存中的值就發生變化了,即主內存與var5的值不一致,則不修改,返回false,while條件知足,繼續執行do操做,再次獲取當前內存中最新的值,繼續判斷,直至保證加一時拿到的結果時最新的。
以上操做簡單說,就是拿到var5,而後對var5+1,擔憂加一操做時var5的值已經發生變化,因此加一時要再驗證一遍,若是這個內存中的值仍是var5那麼就放心的加一操做,不然就不停的去獲取最新的值。

下面舉個例子來具體解釋上一段內容:

假設線程A和線程B兩個線程同時執行getAndAddInt操做:

  1. AtomicInteger裏面的value原始值爲3,即主內存中AtomicInteger中的value爲3,根據JMM模型(參考上篇文章對volatile的理解),線程A和線程B各自持有值爲3的value副本分別到各自的工做內存。
  2. 線程A經過getIntVolatile(var1,var2)拿到value值爲3,這是A被掛起。
  3. 線程B也經過getIntVolatile(var1,var2)拿到value值爲3,此時恰好相線程B沒有被掛起並執行compareAndSwapInt方法比較內存值也是3,成功修改內存值爲4,線程B完成操做。
  4. 這時線程A恢復,並執行compareAndSwapInt方法,發現本身工做內存中的數字3與主內存中的數字4不一致,說明該值已經被其餘線程搶先修改過了,那麼A線程本次修改失敗,從新讀取最新值重來一遍,即從新執行do操做。
  5. 線程A從新獲取value值,由於變量value被volatile修飾,因此其餘線程對他的修改,線程A總能第一時間看到,線程A執行compareAndSwapInt進行比較替換,直至成功。

再概述一下,CAS有3個操做數,內存值V,舊的預期值A,要修改的更新值B。當且僅當預期值A和內存值V相同時,將內存值V修改成B,不然什麼都不作。 這時再去看開篇的那句話是否就理解了呢。

CAS的缺點

  1. 循環時間長開銷很大
    • 根據源碼能夠看到,內部採用do-while結構,若是while條件不知足則一直嘗試獲取新的值,因爲CAS沒有鎖,可能有多個線程同時來修改內存中的值,當多個線程同時在do代碼塊中不斷循環時,給CPU帶來很大的開銷。而synchronized則保證同一時間只有一個線程在真正修改內存中的值。
  2. 只能保證一個共享變量的原子操做
    • 因爲getAndAddInt方法中的第一個參數是object,即this 表示當前對象,從數量上來講這裏只能控制一個共享變量。而synchronized加鎖能鎖住一段代碼,能夠保證多個線程都被鎖住。
  3. 引出來ABA問題
    • ABA問題:線程A要將5修改成10,如今get到內存中的值爲5,這是被掛起;線程B將內存中的值從5改成6,而後又從6改成5,B的一頓操做事後A獲取資源繼續進入while判斷,發現內存中的值仍是5跟本身手裏持有的值相同,能夠進行更新操做將5更爲10。在A看來get和set操做中間沒有發生變化,get的是5,set的時候仍是5,實際上裏面的值已經發生過變化了。儘管線程A的CAS操做成功,可是不表明這個過程就沒有問題。
相關文章
相關標籤/搜索