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個副本出來。

 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方法

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方法

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適用於讀操作遠多於修改操作的並發場景中。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM