多線程十之CopyOnWriteArrayList源碼分析

[TOC]java

簡介

  咱們都很熟悉容器對象ArrayList,而且在初學時就被告知ArrayList不是線程安全的:當咱們在使用迭代器遍歷ArrayList時,若是有其餘線程修改了ArrayList對象,那麼就會拋出ConcurrentModificationException異常。相較於Vector使用synchronized加鎖保證線程安全性,JUC提供了多線程版「ArrayList」:CopyOnWriteArrayList。下面是JDK對CopyOnWriteArrayList的介紹:數組

A thread-safe variant of ArrayList in which all mutative operations (add, set, and so on) are implemented by making a fresh copy of the underlying array. This is ordinarily too costly, but may be more 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 ConcurrentModificationException. The iterator will not reflect additions, removals, or changes to the list since the iterator was created. Element-changing operations on iterators themselves (remove, set, and add) are not supported. These methods throw UnsupportedOperationException.安全

大意是CopyOnWriteArrayList是線程安全版本的ArrayList,全部對CopyOnWriteArrayList修改的操做都是在內部數組的拷貝上進行操做的,這樣作雖然內存花費大,可是在遍歷操做大於修改操做時這樣效率更高,能夠有效防止拋出ConcurrentModificationException異常,迭代器迭代期間不支持對元素更改操做,不然會拋出UnsupportedOperationException異常。數據結構

類結構

  CopyOnWriteArrayList實現了List接口,List表示是有序的Collection,即它用某種特定的插入順序來維護元素順序;實現了標記接口RandomAccess接口支持快速訪問;實現了Iterable接口可使用迭代器遍歷容器元素。多線程

源碼解析

構造方法

  CopyOnWriteArrayList互斥鎖用於對修改容器元素階段加鎖,被volatile修飾的Object數組是CopyOnWriteArrayList存儲數據的底層數據結構,經過volatile保證可以讀到其餘線程對CopyOnWriteArrayList數據的修改,對於數組的訪問都是經過getArray/setArray方法。   CopyOnWriteArrayList提供了三個重載的構造函數,無參構造函數會調用setArray方法構造一個空的Object數組,另外兩個構造函數分別傳入集合/數組參數,將集合/數組內元素存入CopyOnWriteArrayList的底層Object數組。dom

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    private static final long serialVersionUID = 8673264195747942595L;

    //互斥鎖
    final transient ReentrantLock lock = new ReentrantLock();

    //底層存儲數據數組,只能經過getArray/setArray訪問設置,volatile動態數組
    private transient volatile Object[] array;

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

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

    public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }

	//傳入Collection集合對象,將集合中元素存入CopyOnWriteArrayList
    public CopyOnWriteArrayList(Collection<? extends E> c) {
        Object[] elements;
        if (c.getClass() == CopyOnWriteArrayList.class)
            elements = ((CopyOnWriteArrayList<?>)c).getArray();
        else {
            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);
    }

	//傳入數組
    public CopyOnWriteArrayList(E[] toCopyIn) {
        setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
    }
}

add(E e)

   add方法的做用是把傳入元素添加到鏈表list的末尾。add方法有兩點須要注意:1.在寫入過程使用了互斥鎖,因此同一時間只有一個線程在修改CopyOnWriteArrayList 2.增長元素並非直接在原數組操做,而是在原數組的拷貝數組上添加元素的,添加完成後再調用setArray方法用新數組代替原始數組函數

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

add(int index, E element)

  這個方法的做用是把新元素插入到特定位置,會把原來位置的元素向後擠。過程與上面的add大體相同。性能

public void add(int index, E element) {
		//互斥鎖
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
			//原始數組
            Object[] elements = getArray();
            int len = elements.length;
			//檢查index有效性
            if (index > len || index < 0)
                throw new IndexOutOfBoundsException("Index: "+index+
                                                    ", Size: "+len);
			//拷貝數組
            Object[] newElements;
			//從index到數組末尾要向後移動一位數組元素的個數
            int numMoved = len - index;
			//若是index==length,直接把原數組複製到新數組
            if (numMoved == 0)
                newElements = Arrays.copyOf(elements, len + 1);
			//不然分紅兩段複製,原始數組index前面的元素位置一一對應賦值到新數組,原數組index開始的元素複製到
			//新數組index+1到length+1,至關於依次後移。空出來的index就是新元素插入的位置
            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();
        }
    }

get(int index)

  獲取索引位置爲index位置處的元素,獲取元素過程當中沒有使用互斥鎖上鎖。優化

public E get(int index) {
        return get(getArray(), index);
    }

    @SuppressWarnings("unchecked")
    private E get(Object[] a, int index) {
		//返回數組index處位置
        return (E) a[index];
    }

remove(int index)

  移除index處元素。因爲涉及到修改到對鏈表內元素的修改,所以移除過程會使用互斥鎖上鎖。this

public E remove(int index) {
		//上鎖
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
			//原始數組
            Object[] elements = getArray();
            int len = elements.length;
			//數組index處要移除的元素
            E oldValue = get(elements, index);
			//index+1到數組末尾要移動的元素個數
            int numMoved = len - index - 1;
			//若是要移除的元素在數組末尾(index=len-1),直接複製數組區間[0,len-2]全部元素到新數組
            if (numMoved == 0)
                setArray(Arrays.copyOf(elements, len - 1));
			//若是移除的元素再也不末尾,分紅兩段賦值,首先把[0,index-1]區間元素複製到新數組,再把
			//[index+1,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();
        }
    }

迭代器Iterator遍歷

  CopyOnWriteArrayList支持使用迭代器迭代,使用iterator方法返回COWIterator對象,在迭代過程當中沒有上鎖,也不支持remove/set/add等修改方法。

//返回COWIterator對象
    public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
    }

	//實現迭代器的內部類
    static final class COWIterator<E> implements ListIterator<E> {
        //遍歷時原始數組的快照
        private final Object[] snapshot;
        //迭代器迭代的遊標
        private int cursor;

        private COWIterator(Object[] elements, int initialCursor) {
            cursor = initialCursor;
            snapshot = elements;
        }

        public boolean hasNext() {
            return cursor < snapshot.length;
        }

        public boolean hasPrevious() {
            return cursor > 0;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            if (! hasNext())
                throw new NoSuchElementException();
            return (E) snapshot[cursor++];
        }

        @SuppressWarnings("unchecked")
        public E previous() {
            if (! hasPrevious())
                throw new NoSuchElementException();
            return (E) snapshot[--cursor];
        }

        public int nextIndex() {
            return cursor;
        }
    }

總結

  Copy-On-Write簡稱COW,中文簡稱寫入時複製,是一種程序設計優化策略,具體思想就是對於共享內容作修改操做時,會把共享內容複製出來,在複製內容上修改,修改完成後在返還到原內容上。對於CopyOnWriteArrayList而言,向容器添加元素是先把容器複製一份,向複製的容器添加元素,添加成功後把複製的容器賦值給原容器對象;而對於讀取容器的操做直接在原容器進行操做。CopyOnWriteArrayList利用了COW技術實現讀寫的分離,對於寫操做實行加鎖保證安全性,讀操做不改變容器不需加鎖,相對於Vector對全部操做加鎖來保證安全性的效率更高,適合於讀多寫少的場景。同時在CopyOnWriteArrayList保存數據量較大時,對於容器的寫入因爲複製原容器產生新容器用於寫操做,形成了兩倍的內存消耗,會引起頻繁的垃圾回收,下降性能。

相關文章
相關標籤/搜索