1. 程式人生 > >CopyOnWriteArrayList實現原理以及原始碼解析

CopyOnWriteArrayList實現原理以及原始碼解析

CopyOnWriteArrayList實現原理以及原始碼解析


1、CopyOnWrite容器(併發容器)
Copy-On-Write簡稱COW,是一種用於程式設計中的優化策略。
其基本思路是,從一開始大家都在共享同一個內容,當某個人想要修改這個內容的時候,才會真正把內容Copy出去形成一個新的內容然後再改,這是一種延時懶惰策略。
從JDK1.5開始Java併發包裡提供了兩個使用CopyOnWrite機制實現的併發容器,它們是CopyOnWriteArrayList和CopyOnWriteArraySet。

CopyOnWrite容器即寫時複製的容器。
通俗的理解是當我們往一個容器新增元素的時候,不直接往當前容器新增,而是先將當前容器進行Copy,複製出一個新的容器,然後新的容器裡新增元素,新增完元素之後,再將原容器的引用指向新的容器。
這樣做的好處是我們可以對CopyOnWrite容器進行併發的讀,而不需要加鎖,因為當前容器不會新增任何元素。
所以CopyOnWrite容器是一種讀寫分離的思想,讀和寫不同的容器、最終一致性 以及使用另外開闢空間的思路,來解決併發衝突的思想。

2、CopyOnWriteArrayList資料結構:
	public class CopyOnWriteArrayList<E>
            implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}
        
CopyOnWriteArrayList實現了List介面,List介面定義了對列表的基本操作;
同時實現了RandomAccess介面,表示可以隨機訪問(陣列具有隨機訪問的特性);
同時實現了Cloneable介面,表示可克隆;
同時也實現了Serializable介面,表示可被序列化。
CopyOnWriteArrayList底層使用陣列來存放元素。

2、CopyOnWriteArrayList Add方法:
CopyOnWriteArrayList容器是Collections.synchronizedList(List list)的替代方案,是一個ArrayList的執行緒安全的變體。
基本原理:
初始化的時候只有一個容器,很常一段時間,這個容器資料、數量等沒有發生變化的時候,大家(多個執行緒),都是讀取(假設這段時間裡只發生讀取的操作)同一個容器中的資料,所以這樣大家讀到的資料都是唯一、一致、安全的,但是後來有人往裡面增加了一個數據,這個時候CopyOnWriteArrayList 底層實現新增的原理是先copy出一個容器(可以簡稱副本),再往新的容器裡新增這個新的資料,最後把新的容器的引用地址賦值給了之前那個舊的的容器地址,但是在新增這個資料的期間,其他執行緒如果要去讀取資料,仍然是讀取到舊的容器裡的資料。

CopyOnWriteArrayList中add方法的實現(向CopyOnWriteArrayList裡新增元素),可以發現在新增的時候是需要加鎖的,否則多執行緒寫的時候會Copy出N個副本出來。
    /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
			// 複製出新陣列
            Object[] newElements = Arrays.copyOf(elements, len + 1);
			// 把新元素新增到新數組裡
		   newElements[len] = e;
		   // 把原陣列引用指向新陣列
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }
	
    /**
     * Sets the array.
     */
    final void setArray(Object[] a) {
        array = a;
    }

讀的時候不需要加鎖,如果讀的時候有多個執行緒正在向CopyOnWriteArrayList新增資料,讀還是會讀到舊的資料,因為寫的時候不會鎖住舊的CopyOnWriteArrayList。

    public E get(int index) {
        return get(getArray(), index);
    }
3、remove方法:
    /**
     * Removes the element at the specified position in this list.
     * Shifts any subsequent elements to the left (subtracts one from their
     * indices).  Returns the element that was removed from the list.
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E remove(int index) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            E oldValue = get(elements, index);
            int numMoved = len - index - 1;
            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 {
            lock.unlock();
        }
    }
刪除元素,很簡單,就是判斷要刪除的元素是否最後一個,如果最後一個直接在複製副本陣列的時候,複製長度為舊陣列的length-1即可;
但是如果不是最後一個元素,就先複製舊的陣列的index前面元素到新陣列中,然後再複製舊陣列中index後面的元素到陣列中,最後再把新陣列複製給舊陣列的引用。最後在finally語句塊中將鎖釋放。

4、set方法:
    /**
     * Replaces the element at the specified position in this list with the
     * specified element.
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    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();
        }
    }
5、CopyOnWriteArrayList初始化(構造方法)

    /**
     * Sets the array.把老陣列指向新陣列麼
     */
    final void setArray(Object[] a) {
        array = a;
    }

    /**
     * Creates an empty list.建構函式
     */
    public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }

    public CopyOnWriteArrayList(Collection<? extends E> c) {
        Object[] elements = c.toArray();
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elements.getClass() != Object[].class)
            elements = Arrays.copyOf(elements, elements.length, Object[].class);
        setArray(elements);
    }
	/**
     * Creates a list holding a copy of the given array.
     *
     * @param toCopyIn the array (a copy of this array is used as the
     *        internal array)
     * @throws NullPointerException if the specified array is null
     */
    public CopyOnWriteArrayList(E[] toCopyIn) {
        setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
    }
無論我們用哪一個構造方法建立一個CopyOnWriteArrayList物件,都會建立一個Object型別的陣列,然後賦值給成員array。

6、copyOf函式:

該函式用於複製指定的陣列,擷取或用 null 填充(如有必要),以使副本具有指定的長度。

	public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        @SuppressWarnings("unchecked")
        // 確定copy的型別(將newType轉化為Object型別,將Object[].class轉化為Object型別,判斷兩者是否相等,若相等,則生成指定長度的Object陣列
        // 否則,生成指定長度的新型別的陣列)
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        // 將original陣列從下標0開始,複製長度為(original.length和newLength的較小者),複製到copy陣列中(也從下標0開始)
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }

7、CopyOnWrite的應用場景
CopyOnWrite併發容器用於讀多寫少的併發場景。
比如白名單,黑名單,商品類目的訪問和更新場景,假如我們有一個搜尋網站,使用者在這個網站的搜尋框中,輸入關鍵字搜尋內容,但是某些關鍵字不允許被搜尋。
這些不能被搜尋的關鍵字會被放在一個黑名單當中,黑名單每天晚上更新一次。當用戶搜尋時,會檢查當前關鍵字在不在黑名單當中,如果在,則提示不能搜尋。


8、CopyOnWrite的缺點:
CopyOnWrite容器有很多優點(解決開發工作中的多執行緒的併發問題),但是同時也存在兩個問題,即記憶體佔用問題和資料一致性問題。
1.記憶體佔用問題。
因為CopyOnWrite的寫時複製機制,所以在進行寫操作的時候,記憶體裡會同時駐紮兩個物件的記憶體,舊的物件和新寫入的物件(注意:在複製的時候只是複製容器裡的引用,只是在寫的時候會建立新物件新增到新容器裡,而舊容器的物件還在使用,所以有兩份物件記憶體)。
如果這些物件佔用的記憶體比較大,比如說200M左右,那麼再寫入100M資料進去,記憶體就會佔用300M,那麼這個時候很有可能造成頻繁的Yong GC和Full GC。

針對記憶體佔用問題,可以通過壓縮容器中的元素的方法來減少大物件的記憶體消耗,比如,如果元素全是10進位制的數字,可以考慮把它壓縮成36進位制或64進位制。
或者不使用CopyOnWrite容器,而使用其他的併發容器,如ConcurrentHashMap。

2.資料一致性問題。
CopyOnWrite容器只能保證資料的最終一致性,不能保證資料的實時一致性。所以如果你希望寫入的的資料,馬上能讀到,請不要使用CopyOnWrite容器。


9、總結:
1.CopyOnWriteArrayList適用於讀多寫少的場景
2.在併發操作容器物件時不會丟擲ConcurrentModificationException,並且返回的元素與迭代器建立時的元素是一致的
3.容器物件的複製需要一定的開銷,如果物件佔用記憶體過大,可能造成頻繁的YoungGC和Full GC
4.CopyOnWriteArrayList不能保證資料實時一致性,只能保證最終一致性
5.在需要併發操作List物件的時候優先使用CopyOnWriteArrayList
6.隨著CopyOnWriteArrayList中元素的增加,CopyOnWriteArrayList的修改代價將越來越昂貴,因此,CopyOnWriteArrayList適用於讀操作遠多於修改操作的併發場景中。








參考資料:
JDK API CopyOnWriteArrayList
CopyOnWriteArrayList 原始碼 
http://ifeve.com/java-copy-on-write/

每天努力一點,每天都在進步。