併發容器CopyOnWriteArrayList

Copy-On-Write簡稱COW,是一種用於程序設計中的優化策略。其基本思路是,從一開始大家都在共享同一個內容,當某個人想要修改這個內容的時候,纔會真正把內容Copy出去形成一個新的內容然後再改,這是一種延時懶惰策略。從JDK1.5開始Java併發包裏提供了兩個使用CopyOnWrite機制實現的併發容器,它們是CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWrite容器非常有用,可以在非常多的併發場景中使用到。

什麼是CopyOnWrite容器

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

CopyOnWriteArrayList如何做到線程安全的

CopyOnWriteArrayList使用了一種叫寫時複製的方法,當有新元素添加到CopyOnWriteArrayList時,先從原有的數組中拷貝一份出來,然後在新的數組做寫操作,寫完之後,再將原來的數組引用指向到新數組。

public boolean add(E e) {
    //1、先加鎖
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        //2、拷貝數組
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        //3、將元素加入到新數組中
        newElements[len] = e;
        //4、將array引用指向到新數組
        setArray(newElements);
        return true;
    } finally {
       //5、解鎖
        lock.unlock();
    }
}

可以看出來CopyOnWriteArrayList的整個add操作都是在鎖的保護下進行的。

當有新元素加入的時候,如下圖,創建新數組,並往新數組中加入一個新元素,這個時候,array這個引用仍然是指向原數組的。

image
圖片來自水印

當元素在新數組添加成功後,將array這個引用指向新數組。

image
圖片來自水印

讀取操作:

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

但是它的讀操作並沒有同步,因此讀取它的數據的時候不一定是最新的數據。

總結

  1. 注意內存的消耗,每次進行寫入操作的時候,都會複製一個副本,如果這些對象佔用的內存比較大,比如說200M左右,那麼再寫入100M數據進去,內存就會佔用300M,那麼這個時候很有可能造成頻繁的Yong GC和Full GC。針對內存佔用問題,可以通過壓縮容器中的元素的方法來減少大對象的內存消耗,比如,如果元素全是10進制的數字,可以考慮把它壓縮成36進制或64進制。或者不使用CopyOnWrite容器,而使用其他的併發容器,如ConcurrentHashMap。
  2. 不能用於實時讀的場景,像拷貝數組、新增元素都需要時間,所以調用一個set操作後,讀取到數據可能還是舊的,雖然CopyOnWriteArrayList 能做到最終一致性,但是還是沒法滿足實時性要求;
  3. CopyOnWriteArrayList 合適讀多寫少的場景,不過這類慎用
    因爲誰也沒法保證CopyOnWriteArrayList 到底要放置多少數據,萬一數據稍微有點多,每次add/set都要重新複製數組,這個代價實在太高昂了。在高性能的互聯網應用中,這種操作分分鐘引起故障。
  4. 設計思想:併發時候,可以開闢新的地址,來解決併發問題。

參考文章:
* 線程安全的CopyOnWriteArrayList介紹
* Java併發編程:併發容器之CopyOnWriteArrayList(轉載)

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