jdk原始碼分析之CopyOnWriteArrayList

王世暉發表於2016-05-25

CopyOnWriteArrayList的原理

CopyOnWriteArrayList的核心思想是利用高併發往往是讀多寫少的特性,對讀操作不加鎖,對寫操作,先複製一份新的陣列,在新的陣列上面修改,然後將新陣列賦值給舊陣列的引用,並通過volatile 保證其可見性,通過Lock保證併發寫。

底層資料結構

private volatile transient Object[] array;
final Object[] getArray() {
        return array;
final void setArray(Object[] a) {
        array = a;
    }   

底層採用Object陣列儲存資料
陣列使用volatile修飾保證可見性,不讀快取直接讀寫記憶體
陣列使用private修飾限制訪問與,只能通過getter和setter訪問
陣列使用transient修飾,表示序列化時忽略此欄位(自己定製序列化操作)

定製序列化操作

因為Object陣列被transient修飾,因此需要CopyOnWriteArrayList類自己制定序列化方案
方法writeObject和readObject處理物件的序列化。如果宣告該方法,它將會被ObjectOutputStream呼叫而不是採用預設的序列化方案。ObjectOutputStream使用了反射來尋找是否宣告瞭這兩個方法。因為ObjectOutputStream使用getPrivateMethod,所以這些方法不得不被宣告為priate以至於供ObjectOutputStream來使用。
在兩個方法的開始處,呼叫了defaultWriteObject()和defaultReadObject()。它們做的是預設的序列化程式,就像寫/讀所有的non-transient和 non-static欄位(但他們不會去做serialVersionUID的檢查).通常說來,所有我們想要自己處理的欄位都應該宣告為transient。這樣的話,defaultWriteObject/defaultReadObject便可以專注於其餘欄位,而我們則可為被transient修飾的欄位定製序列化。

     /**
     * Saves the state of the list to a stream (that is, serializes it).
     */
    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
        s.defaultWriteObject();

        Object[] elements = getArray();
        // Write out array length
        s.writeInt(elements.length);

        // Write out all elements in the proper order.
        for (Object element : elements)
            s.writeObject(element);
    }

先呼叫s.defaultWriteObject()對非transient修飾的欄位進行序列化操作
然後序列化寫入陣列的長度,再迴圈寫入陣列的元素

     /**
     * Reconstitutes the list from a stream (that is, deserializes it).
     */
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();
        // bind to new lock
        resetLock();

        // Read in array length and allocate array
        int len = s.readInt();
        Object[] elements = new Object[len];

        // Read in all elements in the proper order.
        for (int i = 0; i < len; i++)
            elements[i] = s.readObject();
        setArray(elements);
    }

反序列化的時候先呼叫s.defaultReadObject()恢復沒有被transient修飾的欄位
然後為反序列化得到的CopyOnWriteArrayList物件建立一把新鎖
接著恢復陣列的長度,根據陣列的長度建立一個Object的陣列
然後恢復陣列的每一個元素

讀操作不加鎖

    public E get(int index) {
        return get(getArray(), index);
    }
    private E get(Object[] a, int index) {
        return (E) a[index];
    }

讀操作是直接通過getArray方法獲取Object陣列,然後通過下標index直接訪問資料。讀操作並沒有加鎖,也沒有併發的帶來的問題,因為寫操作是加鎖寫陣列的副本,寫操作成功將副本替換為原資料,這也是寫時複製名字的由來。

加鎖寫副本

 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();
        }
    }

set方法先通過lock加鎖,然後獲取index位置的舊資料,供最後方法返回使用

E oldValue = get(elements, index);

接著建立陣列的副本,在副本上進行資料的替換

Object[] newElements = Arrays.copyOf(elements, len);

Arrays.copyOf(elements, len)方法將會從elements陣列複製len個資料建立一個新的陣列返回
然後在新陣列上進行資料替換,然後將新陣列設定為CopyOnWriteArrayList的底層陣列

newElements[index] = element;
setArray(newElements);

最後在finally塊裡邊釋放鎖

特定位置新增資料

  public void add(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            if (index > len || index < 0)
                throw new IndexOutOfBoundsException("Index: "+index+
                                                    ", Size: "+len);
            Object[] newElements;
            int numMoved = len - index;
            if (numMoved == 0)
                newElements = Arrays.copyOf(elements, len + 1);
            else {
                newElements = new Object[len + 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index, newElements, index + 1,
                                 numMoved);
            }
            newElements[index] = element;
            setArray(newElements);
        } finally {
            lock.unlock();
        }
    }

新增資料和替換資料類似,先加鎖,然後陣列下標檢查,接著建立陣列副本,在副本里邊新增資料,將副本設定為CopyOnWriteArrayList的底層陣列

相關文章