原始碼|併發一枝花之CopyOnWriteArrayList

monkeysayhi發表於2019-01-16

CopyOnWriteArrayList的設計思想非常簡單,但在設計層面有一些小問題需要注意。

JDK版本:oracle java 1.8.0_102

本來不想寫的,但是github上CopyOnWriteArrayList的code results也有165k,為了流量還是寫一寫吧。

實現

看兩個方法你就懂了。

讀元素set()

public E get(int index) {
    return get(getArray(), index);
}
final Object[] getArray() {
    return array;
}
複製程式碼

get()方法直接呼叫內部的getArray()方法,而getArray()方法則直接返回成員變數array。

我沒明白為什麼要再封裝一層,而不是直接訪問。

array指向一個陣列,是CopyOnWriteArrayList的內部資料結構:

private transient volatile Object[] array;
複製程式碼

敲黑板!!!

**array是一個volatile變數,**其讀、寫操作具有Happends-Before關係。具體來講,執行緒W1通過set()方法“修改”集合後,執行緒R1能立刻通過get()方法得到array的最新值。

你可以理解為volatile變數的讀、寫是原子的,不過,我更希望你能從順序和可見性的角度理解理解volatile、鎖等具有偏序關係的操作。volatile的原理和用法見volatile關鍵字的作用、原理

寫元素set()

重點是set()方法:

public E set(int index, E element) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        E oldValue = get(elements, index);

        if (oldValue != element) {
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len);
            newElements[index] = element;
            setArray(newElements);
        } else {
            // Not quite a no-op; ensures volatile write semantics
            setArray(elements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}
final void setArray(Object[] a) {
    array = a;
}
複製程式碼

set()方法也很簡單,兩個要點:

  1. 通過鎖lock保護佇列修改過程
  2. 在副本上修改,最後替換array引用

按照獨佔鎖的思路,僅僅給寫執行緒加鎖是不行的,會有讀、寫執行緒的競爭問題。但是get()中明明沒有加鎖,為什麼也沒有問題呢?

通過加鎖,保證同一時間最多隻有一個寫執行緒W1進入try block;假設要設定的值與舊值不同。9-10行首先將資料複製一份(此時,沒有其他寫執行緒能進入try block修改集合),11行在副本上修改相應元素,12行修改array引用。array是volatile變數,所以寫的最新值對其他讀執行緒、寫執行緒都是可見的。

這就是所謂的“寫時複製”。

其他問題

15行volatile寫的作用

實際上,15行的volatile寫是多餘的。這只是為了能從程式碼裡理解到volatile寫的語義,並不必要的保證什麼——不過這種考慮也是不恰當的,反而使程式碼迷惑。一個類似的例子是addIfAbsent():

public boolean addIfAbsent(E e) {
    Object[] snapshot = getArray();
    return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
        addIfAbsent(e, snapshot);
}
private boolean addIfAbsent(E e, Object[] snapshot) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] current = getArray();
        int len = current.length;
        if (snapshot != current) {
            // Optimize for lost race to another addXXX operation
            int common = Math.min(snapshot.length, len);
            for (int i = 0; i < common; i++)
                if (current[i] != snapshot[i] && eq(e, current[i]))
                    return false;
            if (indexOf(e, current, common, len) >= 0)
                    return false;
        }
        Object[] newElements = Arrays.copyOf(current, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}
複製程式碼

基本思想相同,17、19行都是直接返回,並沒有做多餘的“volatile寫”。

在網上搜的話,還有很多其他觀點。如果你認為我的觀點是錯誤的,歡迎交流。

addIfAbsent的編碼風格跟set()區別很大,不像一個人寫的。需要認識到,JDK是一個發展、變化的產品,一個包、甚至一個類都可能不是同一個人、同一段時間寫的,編碼風格、設計思想可能發生變化;更不要假定JDK的實現一定是對的(當然,絕大部分時候是對的),要基於正確的邏輯去分析,再做判斷。

為什麼必須要給set加鎖?

TODO 20171024

看起來,如果不給set加鎖,似乎併發效能更高,一致性也沒有削弱多少。未解決,歡迎交流。

設計思想

最後總結CopyOnWriteArrayList的設計思想:

  • 用併發訪問“陣列副本的引用”代替併發訪問“陣列元素的引用”,大大降低了維護執行緒安全的難度。
  • 當前副本可能是失效的,但一定是集合在某一瞬間的快照(一定程度上滿足不變性),滿足弱一致性。

本文連結:原始碼|併發一枝花之CopyOnWriteArrayList
作者:猴子007
出處:monkeysayhi.github.io
本文基於 知識共享署名-相同方式共享 4.0 國際許可協議釋出,歡迎轉載,演繹或用於商業目的,但是必須保留本文的署名及連結。

相關文章