CopyOnWriteArrayList使用場景和源碼分析

 (1)ArrayList和CopyOnWriteArrayList的增刪改查實現原理?
 (2)爲什麼說ArrayList查詢快而增刪慢?
 (3)弱一致性的迭代器原理是怎麼樣的?
 (4)CopyOnWriteArrayList爲什麼併發安全且性能比Vector好?
 (5)JDK中爲什麼沒有CopyOnWriteLinkedList?

        對於CopyOnWriteArrayList面試的話,容易和ArrayList放在一起,讓你比較他們之間的異同點,這一點需要有所準備。本文將重點講解CopyOnWriteArrayList,對於這個JUC下面的包,其源碼是很簡單的。

       對於ArrayList在併發情況下使用是不安全的,看一下這個例子:

    @Test
    public void testList(){
        List<String> list = Lists.newArrayList("a", "b", "c", "d");
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        Iterator<String> iterator = list.iterator();
        for (int i = 0; i < 1; i++) {
            threadPool.execute(()->{
                for (int j = 0; j < 5; j++) {
                    list.add( j * 8 + "");
                }
            });
        }

        for (int i = 0; i < 10; i++) {
            threadPool.execute(()->{
                while (iterator.hasNext()){
                    System.out.println(iterator.next());
                }
            });
        }
    }

        以上是因爲有一個線程在向list中添加數據,有五個線程從list中遍歷讀取數據,這個時候就會報錯,其錯誤信息如下:

         

       對於這種可以使用線程安全的List,如Collections.synchronizedList,但是對於那種一年可能也不改一次,而經常需要去進行讀取遍歷的場景來說,簡直太坑了,所以這個時候就出來了CopyOnWriteArrayList這個集合類。

        CopyOnWriteArrayList使用到了Copy-On-Write思想,寫入時複製,這個技術,是一個寫入時複製的容器,它是如何工作的呢?簡單來說,就是平時查詢的時候,都不需要加鎖,隨便訪問,只有在寫入/刪除的時候,纔會從原來的數據複製一個副本出來,然後修改這個副本,最後把原數據替換成當前的副本。修改操作的同時,讀操作不會被阻塞,而是繼續讀取舊的數據。這點要跟讀寫鎖區分一下。

        在CopyOnWriteArrayList開頭有這樣一段註釋:

A thread-safe variant of {@link java.util.ArrayList} in which all mutative operations ({@code add}, {@code set}, and so on) are implemented by making a fresh copy of the underlying array.

 <p>This is ordinarily too costly, but may be <em>more</em> efficient than alternatives when traversal operations vastly outnumber mutations, and is useful when you cannot or don't want to synchronize traversals, yet need to preclude interference among concurrent threads.  The "snapshot" style iterator method uses a reference to the state of the array at the point that the iterator was created. This array never changes during the lifetime of the iterator, so interference is impossible and the iterator is guaranteed not to throw {@code ConcurrentModificationException}.
 The iterator will not reflect additions, removals, or changes to the list since the iterator was created.  Element-changing operations on iterators themselves ({@code remove}, {@code set}, and {@code add}) are not supported. These methods throw {@code UnsupportedOperationException

         不想看英文的可以看以下使用翻譯的中文:

       CopyOnWriteArrayList是ArrayList的一種線程安全的變體,在add、set、remove等會改變其內部值和長度的時候會通過創建一個新的數組來進行實現。
       這通常代價太高,但是當遍歷操作的數量遠遠超過修改數量時,可能比替代操作的效率更高,並且在不希望在同步遍歷時受到其他併發線程的干擾是非常有用的。“快照”樣式的迭代器方法使用對迭代器創建時數組狀態的引用。在迭代器的生存期內,這個數組永遠不會改變,因此干擾是不可能的,並且迭代器保證不會拋出{@code ConcurrentModificationException}。
迭代器不會反映自迭代器創建以來對列表的添加、刪除或更改。不支持對迭代器本身({@code remove}、{@code set}和{@code add})執行元素更改操作。這些方法拋出{@code unsupportedperationexception。

        對於CopyOnWriteArrayList來說,讀的時候不加鎖,只有在寫的時候才加鎖,適用於讀操作遠遠大於寫操作、而且不希望讀的時候也去加鎖,不希望在同步遍歷時受到其他併發線程的干擾而錯誤錯誤的場景。在這種場景下使用CopyOnWriteArrayList非常適合。

        下面看一下CopyOnWriteArrayList的類繼承關係:

                  

         實現了RandomAccess接口,說明支持順序方法(RandomAccess接口沒有任何方法,只是一個標記而已),使用了Cloneable說明可以進行拷貝,實現Serializable接口說明支持序列化,實現List接口說明可以CopyOnWriteArrayList會實現List的所有方法。

         

         首先看一下CopyOnWriteArrayList的屬性,重要的就兩個,使用ReentrantLock來對寫操作進行加鎖,使用volatile修飾的Object[]保存數據,下面先看一下讀操作CopyOnWriteArrayList是怎麼做的:

    /**
     * {@inheritDoc}
     *
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
        return get(getArray(), index);
    }
    
    public boolean contains(Object o) {
        Object[] elements = getArray();
        return indexOf(o, elements, 0, elements.length) >= 0;
    }    
    
    public int size() {
        return getArray().length;
    }
    
    public int indexOf(Object o) {
        Object[] elements = getArray();
        return indexOf(o, elements, 0, elements.length);
    }
    
    private E get(Object[] a, int index) {
        return (E) a[index];
    }   
     
    private E get(Object[] a, int index) {
        return (E) a[index];
    }
    
    private static int indexOf(Object o, Object[] elements,
                               int index, int fence) {
        if (o == null) {
            for (int i = index; i < fence; i++)
                if (elements[i] == null)
                    return i;
        } else {
            for (int i = index; i < fence; i++)
                if (o.equals(elements[i]))
                    return i;
        }
        return -1;
    }

        可以看到讀操作完全沒有加鎖,這樣查詢的性能會很高,最終都是查詢Object[]數組裏面保存的數據。

        下面來看一下add方法:

    /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        //1.先進行加鎖
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            //2.拷貝一個新的數組,此時數據的長度是被拷貝數組長度+1
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            //3.賦值
            newElements[len] = e;
            //4.將新的數組賦值給array
            setArray(newElements);
            return true;
        } finally {
            //5.釋放鎖
            lock.unlock();
        }
    }

    final Object[] getArray() {
        return array;
    }

    final void setArray(Object[] a) {
        array = a;
    }

         可以看到在add操作時,會加鎖,然後進行操作,需要注意一點,Arrays.copyOf會拷貝一個新的數組,屬於深拷貝,會在內存中創建一個數組來存放值,Arrays.copyOf底層是調用native方法進行操作,數組在內存中是一塊連續的區域,這樣做實現拷貝數組的速度會很快,這一點也就能解釋了爲什麼JDK中沒有CopyOnWriteLinkedList的實現,因爲CopyOnWriteLinkedList是鏈表形式的,鏈表的拷貝好像沒有native方法,所以在拷貝時,只能手動在java代碼層面進行操作,這樣的速度在高併發的場景下是不能忍受的,所以JDK中也就沒有CopyOnWriteLinkedList的實現。

        下面來看一下remove方法:

    public E remove(int index) {
        final ReentrantLock lock = this.lock;
        //1.加鎖
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            //2.所以該下標元素
            E oldValue = get(elements, index);
            int numMoved = len - index - 1;
            //3.進行判斷,其需要移除元素的下標在數組的哪個位置,然後進行相應的移除
            if (numMoved == 0)
                setArray(Arrays.copyOf(elements, len - 1));
            else {
                Object[] newElements = new Object[len - 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index + 1, newElements, index,
                                 numMoved);
                setArray(newElements);
            }
            return oldValue;
        } finally {
            //4.釋放鎖
            lock.unlock();
        }
    }

        其操作也是先加鎖,然後進行操作,創建一個新數組,然後將新數組賦值給array。

        對於寫操作的其他方法也是這樣的,操作之前加鎖,操作之後釋放鎖,對於CopyOnWriteLinkedList的源碼實現是很簡單的,主要是理解其需要在什麼場景下面使用,這個纔是關鍵,對於上面5個問題,我就不一一解釋了,其實看到這裏大家都有答案,對於第5條這個在add方法時,也說了。

總結:

        CopyOnWriteArrayList是使用Copy-On-Write思想來進行設計的,在讀操作的時候不加鎖,在寫操作時會進行加鎖,在進行寫操作時,會創建一個數組的副本,所以的操作在這個副本中進行,在操作完成後,將之前的數組給替換成操作完成的副本。適用於讀操作遠遠大於寫操作、而且不希望讀的時候也去加鎖,不希望在同步遍歷時受到其他併發線程的干擾而錯誤錯誤的場景。

      優點:

       對於一些讀多寫少的數據,這種做法的確很不錯,例如配置、黑名單、物流地址等變化非常少的數據,這是一種無鎖的實現。可以幫我們實現程序更高的併發。

       缺點:

       這種實現只是保證數據的最終一致性,在添加到拷貝數據而還沒進行替換的時候,讀到的仍然是舊數據。如果對象比較大,頻繁地進行替換會消耗內存,從而引發Java的GC問題,

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章