CopyOnWriteArrayList分析——能解決什麼問題

CopyOnWriteArrayList主要能夠解決的問題是併發遍歷讀取無鎖(經過Iterator)數組

對比CopyOnWriteArrayList和ArrayList安全

假如咱們頻繁的讀取一個可能會變化的清單(數組),你會怎麼作?數據結構

一個全局的ArrayList(數組),修改時加鎖,讀取時加鎖多線程

讀取時爲何須要加鎖呢?併發

若是是ArrayList遍歷讀取時不加鎖,這時其餘線程修改了ArrayList(增長或刪除),會拋出ConcurrentModificationException,這就是failfast機制(咱們這裏只討論Iterator遍歷,若是是普通for循環可能會數組越界,這裏不討論)dom

若是是數組遍歷讀取時,可能會出現數組越界高併發

因此讀鎖的是寫的操做性能

若是讀加上鎖,那麼對於併發讀來講無疑性能是很糟糕的,固然若是你說用讀寫鎖能夠解決這個問題,可是咱們這裏更期待的是一個無鎖的讀操做而且能保證線程安全。this

下面這個例子營造的背景是相對高併發的讀取+相對低併發的修改spa

List<Integer> arr = new CopyOnWriteArrayList<>();
//List<Integer> arr = new ArrayList<>();//若是經過ArrayList是會報錯的
for (int i = 0; i < 3; i++) {
    arr.add(i);
}
//多線程讀
for (int i = 0; i < 1000; i++) {
    final int m = i;
    new Thread(() -> {
        try {Thread.sleep(1);} catch (InterruptedException e) {}//等等下面寫線程的開始
        Iterator<Integer> iterator = arr.iterator();
        try {Thread.sleep(new Random().nextInt(10));} catch (InterruptedExcep{}//形成不一致的可能性
        int count = 0;
        while(iterator.hasNext()){
            iterator.next();
            count++;
        }
        System.out.println("read:"+count);
    }).start();
}
//多線程寫
for (int ii = 0; ii < 10; ii++) {
    new Thread(() -> {
        arr.add(123);
        System.out.println("write");
    }).start();
}

上面的例子若是更換成ArrayList會報錯,緣由是:

由於next()方法會調用checkForComodification校驗,發現modCount(原始arrayList)與expectedModCount不一致了,這就是上面提到的快速失敗,這個快速失敗的意思是不管當前是否有併發的狀況或問題,只要發現了不一致就拋異常

對於ArrayList解決方案就是遍歷iterator時加鎖

final void checkForComodification() {
  if (modCount != expectedModCount)
     throw new ConcurrentModificationException();
   }

那麼爲何換成CopyOnWriteArrayList就能夠了呢?咱們先不看CopyOnWrite,咱們先來分析一下CopyOnWriteArrayList的iterator

public Iterator<E> iterator() {
     return new COWIterator<E>(getArray(), 0);
}

CopyOnWriteArrayList 調用iterator時生成的是一個新的數組快照,遍歷時讀取的是快照,因此永遠不會報錯(即便讀取後修改了列表),而且在CopyOnWriteArrayList是沒有fastfail機制的,緣由就在於Iterator的快照實現以及CopyOnWrite已經不須要經過fastfail來保證集合的正確性

CopyOnWriteArrayList的CopyOnWrite即修改數組集合時,會從新建立一個數組並對新數據進行調整,調整完成後將新的數組賦值給老的數組

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

爲何要拷貝新的數組,這樣作有什麼好處?

若是不拷貝新的數組(加鎖仍保證其線程安全)直接修改原來的數據結構,那麼在讀的時候就要加鎖了,若是讀不加鎖就有可能讀到修改數組的「半成品」(有可能COWIterator<E>(getArray(), 0);就是個半成品)

而拷貝了新的數組,即便修改沒有完成,遍歷是拿到的也是老的數組,因此不會有問題。

Doug Lea大神在開發這個類的時候也介紹了這個類的主要應用場景是避免對集合的iterator方法加鎖遍歷,咱們來看一下這個類的註釋的節選:

* 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.
* This array never changes during the lifetime of the
* iterator, so interference is impossible and the iterator is
* guaranteed not to throw {@code ConcurrentModificationException}.
* The iterator will not reflect additions, removals, or changes to
* the list since the iterator was created.

大概翻譯一下:

拷貝一個新的數組這看上去太昂貴了,可是遍歷數遠遠超過變動數時卻十分有效,而且在你不想使用synchronized遍歷時會更有用

這份新拷貝的數組在iterator生命週期永遠不會改變,而且在迭代是不會讓生ConcurrentModificationException異常

一旦迭代器建立,則迭代器不可以被修改(添加、刪除元素)

咱們提取一下做者的思想:

一、這個類使用是線程安全的

二、併發經過迭代器遍歷不會報錯而且無鎖

三、在寫少讀多的前提下,比較合適

相關文章
相關標籤/搜索