認識CopyOnWriteArrayList

惜時如金發表於2019-04-08

前言

在這篇文章中,我們將檢視java.util.concurrent包中的CopyOnWriteArrayList。

CopyOnWriteArrayList API

CopyOnWriteArrayList的設計使用一種有趣的技術使其成為執行緒安全的,無需同步。當我們使用任何修改方法時,例如add()或remove(),CopyOnWriteArrayList的全部內容將複製到新的內部副本中。

基於這個原因,我們可以執行緒安全地迭代列表,即使當前有併發修改發生。 當我們在CopyOnWriteArrayList上調研iterator()方法時,我們返回一個由CopyOnWriteArrayList內容的不可變快照備份的Iterator。

其內容是從建立Iterator時開始在ArrayList中的資料的完整副本。即使在此期間其他執行緒新增或刪除列表中的元素,該修改也會生成資料的新副本,該副本將用於從該列表進行的任何進一步資料查詢。

這種資料結構的特性使它特別適用於我們迭代它而不是修改它的情況,如果新增元素是我們場景中的常見操作,那麼CopyOnWriteArrayList將不是一個好的選擇 - 因為額外的副本肯定會導致低於標準的效能。

插入時迭代

首先建立一個儲存整數的CopyOnWriteArrayList。

CopyOnWriteArrayList<Integer> numbers 
  = new CopyOnWriteArrayList<>(new Integer[]{1, 3, 5, 8});
複製程式碼

接著,建立一個迭代器

Iterator<Integer> iterator = numbers.iterator();
複製程式碼

最後,我們向list追加一個元素

numbers.add(10);
複製程式碼

由於我們建立迭代器後,我們拿到了資料的副本。因此,當我們迭代時,我們將看不到10這個值。

List<Integer> result = new LinkedList<>();
iterator.forEachRemaining(result::add);
  
assertThat(result).containsOnly(1, 3, 5, 8);
複製程式碼

再建立一個迭代器後,就可可拿到我們後來追擊的10這個值。

Iterator<Integer> iterator2 = numbers.iterator();
List<Integer> result2 = new LinkedList<>();
iterator2.forEachRemaining(result2::add);
 
assertThat(result2).containsOnly(1, 3, 5, 8, 10);
複製程式碼

在不允許迭代時刪除

建立了CopyOnWriteArrayList是為了允許即使在底層列表被修改時也可以安全地迭代元素。

因為是複製機制,所以不允許對返回的迭代器執行remove()操作,會導致UnsupportedOperationException:

@Test(expected = UnsupportedOperationException.class)
public void whenIterateOverItAndTryToRemoveElement_thenShouldThrowException() {
     
    CopyOnWriteArrayList<Integer> numbers
      = new CopyOnWriteArrayList<>(new Integer[]{1, 3, 5, 8});
 
    Iterator<Integer> iterator = numbers.iterator();
    while (iterator.hasNext()) {
        iterator.remove();
    }
}
複製程式碼