關於Copy On Write Array List,你會安全使用么

摘要:JDK中提供了CopyOnWriteArrayList類,簡稱COW。為了將讀取的性能發揮到極致,CopyOnWriteArrayList讀取是完全不用加鎖的,並且更厲害的是:寫入也不會阻塞讀取操作。

本文分享自華為雲社區《面試官:如何安全地使用List》,作者:李哥技術。

今天我們來討論一個JUC中的集合類CopyOnWriteArrayList。

為什麼研究這個類

在很多應用場景中,對於集合的讀操作的頻率一定會遠遠大於寫操作。由於讀操作根本不會修改原有的數據,因此對於每次讀取都進行加鎖其實是一種資源浪費。我們應該允許多個線程同時訪問List的內部數據,畢竟讀取操作是線程安全的。

JDK中提供了CopyOnWriteArrayList類,簡稱COW。為了將讀取的性能發揮到極致,CopyOnWriteArrayList讀取是完全不用加鎖的,並且更厲害的是:寫入也不會阻塞讀取操作。只有寫入和寫入之間需要進行同步等待。這樣一來,讀操作的性能就會大幅度提升。那它是怎麼做的呢?來吧,讓我們一起研究一下。

設計原理

CopyOnWriteArrayList底層實現是通過Object[]存儲元素的,內部的可變操作(add,set 等方法)都是把數據copy到一個新數組裡,對新數組進行操作,再把新數組賦值給原來的對象,從而達到修改目的。

這樣做的好處是不修改原數組,所以寫操作不會影響到讀操作。

從 CopyOnWriteArrayList 的名字就能看出CopyOnWriteArrayList 是滿足CopyOnWrite 的 ArrayList,所謂CopyOnWrite 也就是說:在計算機,如果你想要對一塊內存進行修改時,我們不在原有內存塊中進行寫操作,而是將內存拷貝一份,在新的內存中進行寫操作,寫完之後呢,就將指向原來內存指針指向新的內存,原來的內存就可以被回收掉了。

定位

public class CopyOnWriteArrayList<E>
 implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
}

從類的繼承關係來看

  1. 實現RandomAccess接口,說明可隨機訪問
  2. 實現Cloneable接口,說明可克隆
  3. 實現了List接口,說明是一個列表
  4. 實現Serializable接口,說明可序列化

接下來讓我們研究一下crud。

public boolean add(E e);// 新增元素,放在數組尾部
public void add(int index, E element);// 新增元素,放在數組指定位置
public boolean addIfAbsent(E e);// 新增元素,如果存在則返回false,如果不存在則放入末尾返回true
public int addAllAbsent(Collection<? extends E> c);// 批量新增元素,將指定集合中尚未包含在此列表中的所有元素附加到此列表的末尾,返回添加的個數
public boolean addAll(Collection<? extends E> c);// 將指定集合中的所有元素附加到此列表的末尾。
public boolean addAll(int index, Collection<? extends E> c);// 從指定位置開始,將當前位於該位置的元素(如果有)和任何後續元素向右移動(增加它們的索引)。新元素將按照指定集合的迭代器返回的順序出現在此列表中。

此函數用於將指定元素添加到此列表的尾部,處理流程如下:

  • 獲取鎖(保證線程安全)
  • 根據Object數組複製一個長度為length+1的Object數組為newElements(此時,newElements[length]為null)
  • 將下標為length的數組元素newElements[length]設置為元素e,再設置當前Object[]為newElements,釋放鎖,返回。這樣就完成了元素的添加。

public E remove(int index);// 移除指定位置的元素,有可能拋出數組越界異常
public boolean remove(Object o);// 移除對象,如果不存在則返回false,存在則移除後返回true
public boolean removeAll(Collection<?> c);// 批量移除指定集合元素,這是一個非常消耗內存的方法,因為內部會額外指定一個臨時數組用來存放需要保留的元素,一共涉及4個數組(老數組、傳入數組、臨時數組、結果數組)
public boolean removeIf(Predicate<? super E> filter);// 和removeAll類似,內部實現需要有臨時數組,也是代價昂貴的方法,請謹慎使用

指定位置刪除的邏輯如下:

  • 獲取鎖
  • 獲取數組,數組長度
  • 獲取指定位置的元素(可能拋出數組越界異常)
  • 計算指定位置的元素是否是當前數組的最後一個
  • 如果是最後一個->不需要挪數據,只需要創建數組,copy數據到數組即可(少copy最後一個),設置數組並返回即可
  • 如果不是最後一個->創建數組,copy 0~index的數據到新數組,再copy (index+1)的數據到新數組,設置數組並返回即可

set方法,修改操作有可能數組越界,這一點需要注意。修改操作也是基於copy的,將數據copy到新數組,對新數組進行替換後再設置數組,從而達到set的目的。

public E get(int index);// 直接從數組中獲取,可能拋出數組越界異常
public Spliterator<E> spliterator();
public Iterator<E> iterator();// 獲取數組的迭代器,它的實現類是COWIterator,內部擁有一個快照的數組屬性
public ListIterator<E> listIterator();// 獲取listIterator迭代器,它的實現類是COWIterator,內部擁有一個快照的數組屬性
public ListIterator<E> listIterator(int index);// 獲取listIterator迭代器,index的作用是設置迭代器當前迭代的位置

先來看一個內部類COWIterator:

COWIterator表示一個迭代器,其也有一個Object類型的數組作為CopyOnWriteArrayList數組的快照,這種快照風格的迭代器方法在創建迭代器時使用了對當時數組狀態的引用。此數組在迭代器的生存期內不會更改,因此不可能發生衝突,並且迭代器保證不會拋出 ConcurrentModificationException。在創建迭代器以後,迭代器就不會反映列表的添加、移除或者更改,因為在迭代器上進行的元素更改操作(remove、set 和 add)不受支持。這些方法將拋出 UnsupportedOperationException。

更深入的理解

CopyOnWriteArrayList每次寫操作都會申請新內存空間,如果數據量較大的話,很容易觸發young gc或者full gc,並且拷貝也會比較消耗內存,雖然適合讀多寫少的應用場景,在互聯網應用中,數據量稍微有點多再操作add或set,非常容易引起故障,還是要謹慎使用。

再談讀,迭代讀的時候是讀取快照數據,只要生成了迭代器,迭代內的快照內容將保證不會發生改變,所以不適合用於實時讀場景。

 

點擊關注,第一時間了解華為雲新鮮技術~