Java系列-CopyOnWriteArrayList源碼解析

CopyOnWriteArrayList 是 ArrayList 的線程安全版本。用一句話歸納它的特色就是:全部的修改操做都是基於副原本進行的。java

設計思想

java.util.concurrent 下,有不少線程安全的容器,大體能夠分紅三類 Concurrent*CopyOnWrite*Blocking*這三類的容器均可以在併發環境下使用,可是實現的方式卻不同。數組

Concurrent* 容器是基於無鎖技術實現,性能很好,ConcurrentHashMap 就是典型表明;CopyOnWrie* 容器則是基於拷貝來實現的,因此對於內存有很大的開銷,CopyOnWriteArrayList 就屬於這一類;Blocking* 容器則使用 鎖技術實現了阻塞技術,在某些場景下很是有用。安全

CopyOnWriteArrayList 的核心操做以下,就是經過不斷的拷貝數組來更新容器:微信

具體實現

CopyOnWriteArrayList 的成員變量以下:多線程

final transient Object lock = new Object();
private transient volatile Object[] array;
複製代碼

變量的數量不多,僅僅包含一個鎖對象和一個用來放元素數組。由於 CopyOnWriteArrayList 保證線程安全的方式很簡單,不斷的經過備份元素來保證數據不會被修改。併發

如何實現線程安全

和其餘線程安全的容器思路不同,這個容器從空間的角度來解決線程安全的問題。全部對容器的修改是基於副本進行的,修改的過程當中也經過鎖對象鎖來保證併發安全,從這個角度來講,CopyOnWriteArrayList 的併發度也不會過高。因此一句話歸納就是使用 synchronized + Array.copyOf 來實現線程安全。函數

迭代器是基於副本進行的,即便原數組被改變,副本也不會被影響。也就不會拋出 ConcurrentModificationException 異常。可是這樣也會讓最新的修改沒法及時體現出來。源碼分析

核心方法的實現

get 方法直接讀取數組就行,不須要上鎖,多個線程同時讀也就不會有併發的問題產生。post

public E get(int index) {
    return elementAt(getArray(), index);
}
static <E> E elementAt(Object[] a, int index) {
    return (E) a[index];
}
複製代碼

下來來看一下 add 方法,代碼很短:性能

// CopyOnWriteArrayList.add()
public boolean add(E e) {
    synchronized (lock) {
        Object[] es = getArray();
        int len = es.length;
        // 複製原數組,而且長度加一
        es = Arrays.copyOf(es, len + 1);
        es[len] = e;
        // 指向新的數組
        setArray(es);
        return true;
    }
}
複製代碼

也就是是說,每次添加元素的時候,都會把原數組複製一次,並把複製後的數組長度加 1,而後把元素添加進數組,最後用新數組去替代舊數組,完成添加。這樣 CopyOnWriteArrayList 根本就不須要擴容,由於每次添加元素都是一個擴容的過程。

// CopyOnWriteArrayList.remove()
public E remove(int index) {
    synchronized (lock) {
        Object[] es = getArray();
        int len = es.length;
        E oldValue = elementAt(es, index);
        // 計算須要移動的元素
        int numMoved = len - index - 1;
        Object[] newElements;
        // 若是刪除的是最後一個元素,則不須要移動
        if (numMoved == 0)
            newElements = Arrays.copyOf(es, len - 1);
        else {
            newElements = new Object[len - 1];
            // 刪除的是中間元素,則須要分兩次複製
            System.arraycopy(es, 0, newElements, 0, index);
            System.arraycopy(es, index + 1, newElements, index, numMoved);
        }
        // 指向新的數組
        setArray(newElements);
        return oldValue;
    }
}
複製代碼

刪除元素的狀況就要複雜一些。刪除的時候若是是刪除中間的元素,須要後面元素進行移動。而後新數組的長度也會減 1,這就至關於縮容過程。

CopyOnWriteArrayList 的迭代器的實現也很不復雜:

# COWIterator 構造函數
COWIterator(Object[] es, int initialCursor) {
    cursor = initialCursor;
    // 容器元素的副本
    snapshot = es;
}
複製代碼

能夠看到,構造迭代器的時候,直接把整個元素的副本都傳進來了,後續的操做都會在這個副本上進行,甚至都須要上鎖。因此是 fail-safe 的。

在 CopyOnWriteArrayList 中,有兩種數組拷貝方式 Arrays.copyOfSystem.arraycopy。這兩種方式有什麼區別嗎?其實是沒有的,來看一下 Arrays.copyOf 的源碼:

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
    T[] copy = ((Object)newType == (Object)Object[].class)
        ? (T[]) new Object[newLength]
        : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    System.arraycopy(original, 0, copy, 0,
                        Math.min(original.length, newLength));
    return copy;
}
複製代碼

沒錯,Arrays.copyOf 調用了 System.arraycopy 來實現數組拷貝。

經過上面的分析可知,CopyOnWriteArrayList 的讀效率很高,可是寫的效率很低,因此比較適合讀多寫少的場景。

另外須要說一句,CopyOnWriteArraySet 使用 CopyOnWriteArrayList 實現。Set 一如繼往喜歡使用現成的類來實現。

原文

相關文章

關注微信公衆號,聊點其餘的

相關文章
相關標籤/搜索