JUC AtomicStampedReference源碼解析 JDK8

前言

大家都知道CAS操作有ABA問題的,且ABA問題是針對引用型對象的,而AtomicStampedReference的出現就是爲了解決這一問題而出現的。通過加版本號來實現。

成員

public class AtomicStampedReference<V> {
    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) {  //靜態方法以創建Pair
            return new Pair<T>(reference, stamp);
        }
    }

    private volatile Pair<V> pair;
...
}
  • 靜態內部類Pair,reference成員保存你的引用型對象,stamp成員則保存你對象的版本號。
  • AtomicStampedReference有一個volatile的Pair<V>成員,這樣,保證了pair成員的可見性。
  • 爲了讓Pair對象不可變,讓兩個域都是final的。你沒有辦法修改Pair對象的成員,所以只能通過靜態函數重新構造一個Pair對象。這一點很重要。
  • 你可能想問,反正靜態內部類Pair是隻給自己用的,何必將Pair定義成泛型類呢,完全可以如下定義。一來這樣就無法使用靜態函數of了,構造器是私有的只能通過這個靜態函數訪問構造器(註釋中有解釋);二來Pair對象並不需要作爲成員內部類而存在,即Pair不需要持有外部類的引用。
public class AtomicStampedReference<V> {
    private class Pair {
        final V reference;  //直接使用V
        final int stamp;  
        private Pair(V reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        //此靜態函數會報錯,因爲這個靜態域需要AtomicStampedReference的對象已存在才能得到它的泛型類型V是什麼
        //一個靜態域,居然依賴一個對象,所以會報錯。
        static Pair of(V reference, int stamp) {  
            return new Pair(reference, stamp);
        }
    }

    private volatile Pair<V> pair;
...
}

常用操作

    public AtomicStampedReference(V initialRef, int initialStamp) {
        pair = Pair.of(initialRef, initialStamp);
    }
    
    V getReference() {
        return pair.reference;
    }

    public int getStamp() {
        return pair.stamp;
    }
  • getReference/getStamp都是通過volatile的成員pair獲得的,所以具有可見性。
    public V get(int[] stampHolder) {
        Pair<V> pair = this.pair;
        stampHolder[0] = pair.stamp;
        return pair.reference;
    }
  • get函數是爲了“一次性”獲得存儲對象和版本號。對象通過返回值,版本號通過int數組。
  • 因爲int作爲參數只能值傳遞,而int數組是一個對象,可以引用傳遞,所以要使用int數組。由於只需要得到版本號,所以數組大小大於1就可以了。
  • 當然,該函數是複合操作,不具有原子性。
    public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            //舊引用是相同的
            expectedReference == current.reference &&
            //舊版本號也是相同的
            expectedStamp == current.stamp &&
            //想設的新引用和新版本號,也和當前的相同
            ((newReference == current.reference && newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }

    private boolean casPair(Pair<V> cmp, Pair<V> val) {
        return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
    }

//Unsafe.java
    public final native boolean compareAndSwapObject(Object o, long offset,
                                                     Object expected,
                                                     Object x);
  • 首先expectedReference == current.reference判斷舊引用,和當前的是否一樣;expectedStamp == current.stamp判斷舊版本,和當前的是否一樣。
  • (newReference == current.reference && newStamp == current.stamp)判斷 新引用和新版本號,是否和當前的相同,一般情況下,不會滿足此判斷的。
  • 既然上一條不滿足,則調用casPair,最終調用到Unsafe的compareAndSwapObject
  • 注意,每次執行此函數時,都會重新構造Pair對象,這一點很關鍵。這保證調用compareAndSwapObject函數時,參數expected和x肯定兩個不同的對象,就算最開始的expectedReference與newReference一樣,且expectedStamp與newStamp一樣,也會構造出一個新對象,當然這由於短路或不會執行到的。
  • 完全有可能,從expectedStamp == current.stampcasPair(current, Pair.of(newReference, newStamp))期間,線程被切換走了,切換的時候pair成員變量已經被修改了,等切換回來,current這個局部變量就不和成員pair相等了,自然casPair會失敗。
    public void set(V newReference, int newStamp) {
        Pair<V> current = pair;
        if (newReference != current.reference || newStamp != current.stamp)
            this.pair = Pair.of(newReference, newStamp);
    }
  • 這是用來無條件設置的函數。因爲不需要保持舊值是否相同。
  • 如果 新引用和新版本號 和 當前的 一樣,那麼不需要更新pair成員。
    public boolean attemptStamp(V expectedReference, int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            (newStamp == current.stamp ||
             casPair(current, Pair.of(expectedReference, newStamp)));
    }
  • 此函數只更新版本號,不更新引用對象。
  • 只需保證舊引用和當前的相同,casPair函數保證了 只有current局部變量與當前pair成員是同一個對象時,才更新。
    private static final sun.misc.Unsafe UNSAFE = sun.misc.Unsafe.getUnsafe();
    private static final long pairOffset =
        objectFieldOffset(UNSAFE, "pair", AtomicStampedReference.class);

    static long objectFieldOffset(sun.misc.Unsafe UNSAFE,
                                  String field, Class<?> klazz) {
        try {
            return UNSAFE.objectFieldOffset(klazz.getDeclaredField(field));
        } catch (NoSuchFieldException e) {
            // Convert Exception to corresponding Error
            NoSuchFieldError error = new NoSuchFieldError(field);
            error.initCause(e);
            throw error;
        }
    }
  • 獲得pair域的偏移量。

總結

  • 最終調用Unsafe的compareAndSwapObject方法時,是不關心版本號的。compareAndSwapObject只關心是不是同一個對象。(但這樣不會造成問題)
  • 雖然根據上一條,感覺可能會有問題。但是由於靜態內部類Pair每次都會新構造對象出來,即使T reference, int stamp兩個參數完全一樣,也會構造出兩個不同的對象,所以最終調用的compareAndSwapObject不關心版本號也沒有關係。
  • 綜上,版本號是在Unsafe的CAS操作上進行的附加判斷,準備的說,是先判斷版本號,再通過CAS操作判斷對象。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章