CopyOnWriteArrayList原理解析

CopyOnWriteArrayList是一個線程安全的ArrayList,對其進行的修改操作都是在底層的一個複製的數組(快照)上進行的,也就是使用了寫時複製策略。如圖所示是CopyOnWriteArrayList的類圖結構:
類圖
上圖有個小瑕疵,lock 是 包級私有,而不是 protected。
能夠看到,每個CopyOnWriteArrayList對象都有一個array數組用來存放具體元素,而ReenTrantLock則用來保證只有一個線程對Array進行修改。ReenTrantLock本身是一個獨佔鎖,同時只有一個線程能夠獲取。接下來看一下其中的一些方法代碼。

初始化

共有三個構造函數:

	public CopyOnWriteArrayList() {
        setArray(new Object[0]);			//創建一個大小爲0的Object數組作爲array初始值
    }
	public CopyOnWriteArrayList(E[] toCopyIn) {
		//創建一個list,其內部元素是toCopyIn的的副本
        setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
    }
	//將傳入參數集合中的元素複製到本list中
	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);
    }

setArray方法很簡單:

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

添加元素

添加元素有很多方法,包括add(E e), add(int index, E element)等,原理基本上相同,所以我們只看add(E e)的源碼。

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

代碼很簡單,就是將原來的元素複製到了一個新數組中,且長度應該加1,然後在新數組末尾加上要添加的元素,最後設置新數組爲自己的array。

獲取指定位置元素

使用E get(int index)方法獲取下標爲index的元素:

	public E get(int index) {
        return get(getArray(), index);
    }
	
	final Object[] getArray() {
        return array;
    }

	private E get(Object[] a, int index) {
        return (E) a[index];
    }

這個方法是線程不安全的,因爲這個分成了兩步,分別是獲取數組和獲取元素,而且中間過程沒有加鎖。假設當前線程在獲取數組(執行getArray())後,其他線程修改了這個CopyOnWriteArrayList,那麼它裏面的元素就會改變,但此時當前線程返回的仍然是舊的數組,所以返回的元素就不是最新的了,這就是寫時複製策略產生的弱一致性問題

修改指定元素

使用E set (int index, E element)修改list中指定元素的值,代碼如下:

    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;
                //將array複製到新數組,並進行修改,並設置array爲新數組
                Object[] newElements = Arrays.copyOf(elements, len);			
                newElements[index] = element;
                setArray(newElements);
            } else {
                // 雖然值確實沒改,但要保證volatile語義,需重新設置array
                setArray(elements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }

刪除元素

使用public E remove(int index)方法,代碼如下:

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

也很簡單,就是將元素分兩次複製到新數組中,然後設置array爲新數組。返回的是刪除的元素。

弱一致性的迭代器

我們先看一下迭代器是怎麼使用的:

    public static void main(String[] args) {
        CopyOnWriteArrayList<String> arrayList = new CopyOnWriteArrayList<>();
        arrayList.add("hello");
        arrayList.add("alibaba");

        Iterator<String> itr = arrayList.iterator();
        while (((Iterator) itr).hasNext())
            System.out.println(itr.next());
    }

很簡單,那弱一致性是怎麼回事呢,它是指返回迭代器後,其他線程對list的增刪改對迭代器是不可見的。接下來看一下爲什麼會這樣:

    public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);	//返回一個COWIterator對象
    }
    static final class COWIterator<E> implements ListIterator<E> {
        /** 數組array快照 */
        private final Object[] snapshot;
        /** 數組下標  */
        private int cursor;

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

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

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

在調用iterator()方法後,會返回一個COWIterator對象,COWIterator對象的snapshot變量保存了當前list的內容,cursor是遍歷list時數據的下標。

那麼爲什麼說snapshot是list的快找呢,明明傳的是引用。其實這就和CopyOnWriteArrayList本身有關了,如果在返回迭代器後沒有對裏面的數組array進行修改,則這兩個變量指向的確實是同一個數組;但是若修改了,則根據前面所講,它是會新建一個數組,然後將修改後的數組複製到新建的數組,而老的數組就會被“丟棄”,所以如果修改了數組,則此時snapshot指向的還是原來的數組,而array變量已經指向了新的修改後的數組了。這也就說明獲取迭代器後,使用迭代器元素時,其他線程對該list的增刪改不可見,因爲他們操作的是兩個不同的數組,這就是弱一致性

接下來就演示一下這個現象:

public class copylist {

    private static volatile CopyOnWriteArrayList<String> arrayList = new CopyOnWriteArrayList<>();

    public static void main(String[] args) throws InterruptedException{
        arrayList.add("hello");
        arrayList.add("alibaba");
        arrayList.add("welcome");
        arrayList.add("to");
        arrayList.add("hangzhou");

        Thread threadOne = new Thread(new Runnable() {
            @Override
            public void run() {
                arrayList.set(1, "baba");
                arrayList.remove(2);
                arrayList.remove(3);
            }
        });

        Iterator<String> itr = arrayList.iterator();

        threadOne.start();
        threadOne.join();

        while (itr.hasNext())
            System.out.println(itr.next());
    }
}

運行結果如下,說明雖然線程threadOne改變了這個list,但是獲取了迭代器後,它指向的還是舊的數組,所以遍歷的時候還是舊的數組內容。所以==獲取迭代器的操作必須在子線程操作之前進行。

hello
alibaba
welcome
to
hangzhou

總結

CopyOnWriteArrayList使用寫時複製策略保證list的一致性,而獲取–修改–寫入三個步驟不是原子性,所以需要一個獨佔鎖保證修改數據時只有一個線程能夠進行。另外,CopyOnWriteArrayList提供了弱一致性的迭代器,從而保證在獲取迭代器後,其他線程對list的修改是不可見的,迭代器遍歷的數組是一個快照。

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