1. 程式人生 > >【Java基礎】談談集合.CopyOnWriteArrayList

【Java基礎】談談集合.CopyOnWriteArrayList

目錄

  • 實現原理
  • 遍歷時不用加鎖的原因
  • CopyOnWriteArrayLis的缺點
  • 使用場景
  • 總結
  • 參考

本篇部落格介紹CopyOnWriteArrayList類,讀完本部落格你將會了解:

  • 什麼是COW機制;
  • CopyOnWriteArrayList的實現原理;
  • CopyOnWriteArrayList的使用場景。

經過之前的部落格介紹,我們知道ArrayList是執行緒不安全的。要實現執行緒安全的List,我們可以使用Vector,或者使用Collections工具類將List包裝成一個SynchronizedList。其實在Java併發包中還有一個CopyOnWriteArrayList可以實現執行緒安全的List。

在開始之前先貼一段概念

如果有多個呼叫者(callers)同時請求相同資源(如記憶體或磁碟上的資料儲存),他們會共同獲取相同的指標指向相同的資源,直到某個呼叫者試圖修改資源的內容時,系統才會真正複製一份專用副本(private copy)給該呼叫者,而其他呼叫者所見到的最初的資源仍然保持不變。優點是如果呼叫者沒有修改該資源,就不會有副本(private copy)被建立,因此多個呼叫者只是讀取操作時可以共享同一份資源。

實現原理

Vector這個類是一個非常古老的類了,在JDK1.0的時候便已經存在,其實現安全的手段非常簡單所有的方法都加上synchronized關鍵字,這樣保證這個例項的方法同一時刻只能有一個執行緒訪問,所以在高併發場景下效能非常低。

SynchronizedList是java.util.Collections中的一個靜態內部類,其實現安全的手段稍微有一點優化,就是把Vector加在方法上的synchronized關鍵字,移到了方法裡面變成了同步塊而不是同步方法從而把鎖的範圍縮小了,另外,SynchronizedList中的方法不全都是同步的,比如獲取迭代器方法listIterator()就不是同步的。

CopyOnWriteArrayList這個類就比較特殊了,對於寫來說是基於重入鎖互斥的,對於讀操作來說是無鎖的。還有一個特殊的地方,這個類的iterator是fail-safe的,也就是說是執行緒安全List裡面的唯一一個不會出現ConcurrentModificationException異常的類。

看下CopyOnWriteArrayList的成員變數:

//重入鎖保寫操作互斥
final transient ReentrantLock lock = new ReentrantLock();
//volatile保證讀可見性
private transient volatile Object[] array;

下面再看下新增元素的程式碼邏輯

public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();//加鎖
        try {
            Object[] elements = getArray();//讀取原陣列
            int len = elements.length;
            //構建一個長度為len+1的新陣列,然後拷貝舊資料的資料到新陣列
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            //把新加的資料賦值到最後一位
            newElements[len] = e;
            // 替換舊的陣列
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

先獲得鎖,然後拷貝元素組並將新元素加入(新增的元素可以是null),再替換掉原來的陣列。我們會發現這種實現方式非常不適合頻繁修改的操作。CopyOnWriteArrayList的刪除和修改的操作的原理也是類似的,這邊就不貼程式碼了。

最後看下讀操作

//直接獲取index對應的元素 
public E get(int index) {return get(getArray(), index);} 
private E get(Object[] a, int index) {return (E) a[index];}

從以上的增刪改查中我們可以發現,增刪改都需要獲得鎖,並且鎖只有一把,而讀操作不需要獲得鎖,支援併發。為什麼增刪改中都需要建立一個新的陣列,操作完成之後再賦給原來的引用?這是為了保證get的時候都能獲取到元素,如果在增刪改過程直接修改原來的陣列,可能會造成執行讀操作獲取不到資料。

遍歷時不用加鎖的原因

常用的方法實現我們已經基本瞭解了,但還是不知道為啥能夠在容器遍歷的時候對其進行修改而不丟擲異常。(其實這是一種fail-safe機制)

    // 1. 返回的迭代器是COWIterator
    public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
    }
    // 2. 迭代器的成員屬性
    private final Object[] snapshot;
    private int cursor;
    // 3. 迭代器的構造方法
    private COWIterator(Object[] elements, int initialCursor) {
        cursor = initialCursor;
        snapshot = elements;
    }
    // 4. 迭代器的方法...
    public E next() {
        if (! hasNext())
            throw new NoSuchElementException();
        return (E) snapshot[cursor++];
    }

    //.... 可以發現的是,迭代器所有的操作都基於snapshot陣列,而snapshot是傳遞進來的array陣列

到這裡,我們應該就可以想明白了!CopyOnWriteArrayList在使用迭代器遍歷的時候,操作的都是原陣列!

CopyOnWriteArrayLis的缺點

  • 記憶體佔用:如果CopyOnWriteArrayList經常要增刪改裡面的資料,經常要執行add()、set()、remove()的話,那是比較耗費記憶體的。因為我們知道每次add()、set()、remove()這些增刪改操作都要複製一個數組出來。
  • 資料一致性:CopyOnWrite容器只能保證資料的最終一致性,不能保證資料的實時一致性。從上面的例子也可以看出來,比如執行緒A在迭代CopyOnWriteArrayList容器的資料。執行緒B線上程A迭代的間隙中將CopyOnWriteArrayList部分的資料修改了(已經呼叫setArray()了)。但是執行緒A迭代出來的是原有的資料。

使用場景

整體來說CopyOnWriteArrayList是另類的執行緒安全的實現,但並一定是高效的,適合用在讀取和遍歷多的場景下,並不適合寫併發高的場景,因為陣列的拷貝也是非常耗時的,尤其是資料量大的情況下。

總結

稍微總結下:

  • CopyOnWriteArrayList基於可重入鎖機制,增刪改操作需要加鎖,讀操作不需要加鎖;
  • CopyOnWriteArrayList適合用在讀取和遍歷多的場景下,並不適合寫併發高的場景;
  • 基於fail-safe機制,不會丟擲CurrentModifyException。

參考

  • https://yq.aliyun.com/articles/665359
  • https://cloud.tencent.com/developer/article/1350855