多線程併發 (四) 瞭解原子類 AtomicXX 屬性地址偏移量,CAS機制

章節:
多線程併發 (一) 瞭解 Java 虛擬機 - JVM
多線程併發 (二) 瞭解 Thread
多線程併發 (三) 鎖 synchronized、volatile
多線程併發 (四) 瞭解原子類 AtomicXX 屬性地址偏移量,CAS機制
多線程併發 (五) ReentrantLock 使用和源碼


瞭解了Java虛擬機,線程,鎖,volatile概念之後對多線程開發算是比較熟悉了。解決線程併發產生的問題,除了鎖,volatile等關鍵字之外,在特定的情景下爲了提高代碼運行的效率,爲了擺脫“鎖”這個獨佔式的編程方式之外,還有另外一個原子類的概念。
在java.util.concurrent.atomic包下有Java提供的線程安全的原子類。瞭解 AtomicInteger 和 CAS 機制。

1. AtomicInteger的實現

通過上一篇中volatile的自增的例子,我們知道要想實現這種自贈的效果就需要加鎖,爲了提高效率,這種場景下原子類型就可以勝任。

AtomicIntegerai =new AtomicInteger(1);
ai.incrementAndGet();

查看實現代碼:

 /**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        return U.getAndAddInt(this, VALUE, 1) + 1;
    }

根據incrementAndGet()方法瞭解到AtomicInteger是對U的一個封裝,U就是Unsafe類。

    private static final sun.misc.Unsafe U = sun.misc.Unsafe.getUnsafe();
    private static final long VALUE;
    static {
        try {
            VALUE = U.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (ReflectiveOperationException e) {
            throw new Error(e);
        }
    }
    private volatile int value;

這段代碼首先獲得Unsafe對象,先聲明一下Unsafe是個單例,Unsafe裏面基本都是native方法。
static代碼塊裏面初始化了VALUE這個值,static修飾的類加載的時候就會被初始化,並且引用是放到 Jvm的方法區的屬於類的數據。
繼續VALUE是什麼呢?查看U.objectFieldOffset()方法:

 /**
     * Gets the raw byte offset from the start of an object's memory to
     * the memory used to store the indicated instance field.
     *
     * @param field non-null; the field in question, which must be an
     * instance field
     * @return the offset to the field
     */
    public long objectFieldOffset(Field field) {
        return field.getOffset();
    }

看方法註釋:從對象的內存處開始,獲得原始字節偏移量,用於存儲實力對象的內存。好像還是不理解~。畫個圖:

上幾篇提到對象在內存中的分佈其中有個padding對齊,就是保證一個對象的內存大小必須是8的倍數。在這裏偏移量的意思就像我們 new 一個數組,數組的地址就是數組地一個元素的地址,假如數組地址是 a,第二個元素就是a+1,其中+1就是偏移量。對應的對象的一個屬性的偏移量就是其對象的地址開始增加,增加的數就是這個filed的偏移量。

對於VALUE這個值我們知道了,他是AtomicInteger中的value屬性對應的偏移量,就是對象地址+VALUE = value的地址

繼續看代碼:

 /**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        return U.getAndAddInt(this, VALUE, 1) + 1;
    }
 /**
     * Atomically adds the given value to the current value of a field
     * or array element within the given object {@code o}
     * at the given {@code offset}.
     *
     * @param o object/array to update the field/element in
     * @param offset field/element offset
     * @param delta the value to add
     * @return the previous value
     * @since 1.8
     */
    // @HotSpotIntrinsicCandidate
    public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        do {
            v = getIntVolatile(o, offset);
        } while (!compareAndSwapInt(o, offset, v, v + delta));
        return v;
    }

知道了offset值的意義之後

  1. 繼續向下就是 v = getIntVolatile(o, offset); 這段代碼,這個代碼含義其實就是根據object和屬性在object中的偏移地址,拿到 v(對應的共享內存中的 value 值,通過volatile控制值的可見性)。
  2. compareAndSwapInt(o, offset, v, v + delta) 這個就是CAS(CompareAndSwap)機制 = 先拿着 v(預期的值)和 共享內存的值做比較 如果其他線程沒有修改過就替換掉,否則就一直自旋判斷直到成功。

如果在比較過程中不成功,也就是值被其他線程修改了,這時候CAS機制是一直循環的,這樣無非也會消耗大量CPU。

CAS是如何保證原子性的呢?
看了CAS的java代碼並沒提到他是通過什麼方式保證原子性的,CAS是通過Unsafe類調用C然後調用處理器的指令,大部分處理器都實現了CAS的原子性,對於多核處理器在運行到CAS指令的時候會標記一個lock,當處理器運行到lock這個標記時,其他處理器就處於等待狀態,單核處理器按步驟進行不會影響。另外一種保證原子性的處理器是通過保證在同一時間內當前處理器訪問的共享內存地址不被其他處理器訪問,新的方式提高了效率。
看了幾篇文章 對於偏移量講的不太清楚,所以在這裏按照自己的理解梳理了這個流程,有錯誤的請指出。

2. CAS實現原子性操作的三大問題

這部分引用於:https://www.jianshu.com/p/5ee20d1128da

CAS雖然很高的解決了原子操作,但是CAS仍然存在三大問題。ABA問題、循環時間長開銷大、以及只能保證一個共享變量的原子操作。

  1. ABA問題
    因爲CAS需要在操作值的時候,檢查值有沒有發生變化,如果發生變化則更新,但是如果一個值爲A,變成了B,又變成了A,那麼使用CAS進行檢查時就會發現它的值沒有發生變化,但實際上發生變化了。ABA問題的解決思路就是使用版本號,在變量前邊追加版本號,每次變量更新的時候把版本號加1,那麼A→B→A就會變成1A→2B→3A。舉個通俗點的例子,你倒了一杯水放桌子上,幹了點別的事,然後同事把你水喝了又給你重新倒了一杯水,你回來看水還在,拿起來就喝,如果你不管水中間被人喝過,只關心水還在,這就是ABA問題。
    從java1.5開始,JDK提供了AtomicStampedReference、AtomicMarkableReference來解決ABA的問題,通過compareAndSet方法檢查值是否發生變化以外檢查版本號知否發生變化。

  2. 循環時間長開銷大
    自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷。

  3. 只能保證一個共享變量的原子操作
    當對一個共享變量執行操作時,我們可以使用循環CAS的方式來保證原子操作,但是對多個共享變量操作時,循環CAS就無法保證操作的原子性,這個時候就可以用鎖。
    從Java 1.5開始,JDK提供了AtomicReference類來保證引用對象之間的原子性,就可以把多個變量放在一個對象裏來進行CAS操作。

 

發佈了120 篇原創文章 · 獲贊 147 · 訪問量 19萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章