天天看點

CopyOnWriteArrayList原理CopyOnWriteArrayList原理

CopyOnWriteArrayList原理

文章目錄

  • CopyOnWriteArrayList原理
    • 1、什麼是CopyOnWrite容器
    • 2、CopyOnWriteArrayList的實作原理
    • 3、CopyOnWrite的缺點

1、什麼是CopyOnWrite容器

CopyOnWrite容器即寫時複制的容器。通俗的了解是當我們往一個容器添加元素的時候,不直接往目前容器添加,而是先将目前容器進行Copy,複制出一個新的容器,然後新的容器裡添加元素,添加完元素之後,再将原容器的引用指向新的容器。這樣做的好處是我們可以對CopyOnWrite容器進行并發的讀,而不需要加鎖,因為目前容器不會添加任何元素。是以CopyOnWrite容器也是一種讀寫分離的思想,讀和寫不同的容器。

适用場景:讀多寫少的場景。

2、CopyOnWriteArrayList的實作原理

在使用CopyOnWriteArrayList之前,我們先閱讀其源碼了解下它是如何實作的。以下代碼是向CopyOnWriteArrayList中add方法的實作(向CopyOnWriteArrayList裡添加元素),可以發現在添加的時候是需要加鎖的,否則多線程寫的時候會Copy出N個副本出來。【以下源碼基于jdk1.8】

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

讀的時候不需要加鎖,如果讀的時候有多個線程正在向CopyOnWriteArrayList添加資料,讀還是會讀到舊的資料,因為開始讀的那一刻已經确定了讀的對象是舊對象。

CopyOnWrite并發容器用于讀多寫少的并發場景。比如白名單,黑名單等場景。

3、CopyOnWrite的缺點

(1)記憶體占用問題。因為CopyOnWrite的寫時複制機制,是以在進行寫操作的時候,記憶體裡會同時駐紮兩個對象的記憶體,舊的對象和新寫入的對象(注意:在複制的時候隻是複制容器裡的引用,隻是在寫的時候會建立新對象添加到新容器裡,而舊容器的對象還在使用,是以有兩份對象記憶體)。如果這些對象占用的記憶體比較大,比如說200M左右,那麼再寫入100M資料進去,記憶體就會占用300M,那麼這個時候很有可能造成頻繁的Yong GC和Full GC。之前我們系統中使用了一個服務由于每晚使用CopyOnWrite機制更新大對象,造成了每晚15秒的Full GC,應用響應時間也随之變長。

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

(2)資料一緻性問題。CopyOnWrite容器隻能保證資料的最終一緻性,不能保證資料的實時一緻性。是以如果你希望寫入的的資料,馬上能讀到,請不要使用CopyOnWrite容器。【當執行add或remove操作沒完成時,get擷取的仍

CopyOnWriteArrayList讀取時不加鎖隻是寫入和删除時加鎖,是以一個線程X讀取的時候另一個線程Y可能執行remove操作。remove操作首先要擷取獨占鎖,然後進行寫時複制操作,就是複制一份目前的array數組,然後在複制的新數組裡面删除線程X通過get通路的元素,比如:1。删除完成後讓array指向這個新的數組。

線上程x執行get操作的時候并不是直接通過全局array通路數組元素而是通過方法的形參a通路的,a指向的位址和array指向的位址在調用get方法的那一刻是一樣的,都指向了堆記憶體的數組對象。之後改變array指向的位址并不影響get的通路,因為在調用get方法的那一刻形參a指向的記憶體位址就已經确定了,不會改變。是以讀的仍然是舊數組。然是舊數組的元素】