源碼|併發一枝花之CopyOnWriteArrayList

CopyOnWriteArrayList的設計思想很是簡單,但在設計層面有一些小問題須要注意。java

JDK版本:oracle java 1.8.0_102git

原本不想寫的,可是github上CopyOnWriteArrayList的code results也有165k,爲了流量仍是寫一寫吧。github

實現

看兩個方法你就懂了。數組

讀元素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;
複製代碼

敲黑板!!!oracle

**array是一個volatile變量,**其讀、寫操做具備Happends-Before關係。具體來說,線程W1經過set()方法「修改」集合後,線程R1能馬上經過get()方法獲得array的最新值。app

你能夠理解爲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();
    }
}
複製代碼

基本思想相同,1七、19行都是直接返回,並無作多餘的「volatile寫」。

在網上搜的話,還有不少其餘觀點。若是你認爲個人觀點是錯誤的,歡迎交流。

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

爲何必需要給set加鎖?

TODO 20171024

看起來,若是不給set加鎖,彷佛併發性能更高,一致性也沒有削弱多少。未解決,歡迎交流。

設計思想

最後總結CopyOnWriteArrayList的設計思想:

  • 用併發訪問「數組副本的引用」代替併發訪問「數組元素的引用」,大大下降了維護線程安全的難度。
  • 當前副本多是失效的,但必定是集合在某一瞬間的快照(必定程度上知足不變性),知足弱一致性。

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

相關文章
相關標籤/搜索