Background
寫時複製 (Copy on Write, COW) 有時也叫 “隱式共享”, 顧名思義, 就是讓所有需要使用資源 R 的使用者共享資源 R 的同一個副本, 當其中的某一個使用者要對資源 R 進行修改操作時, 先複製 R 的一個副本 R’ , 再進行修改操作;
Problem
在 Java 集合框架中, 像 ArrayList
, HashSet
等基礎集合類是非執行緒安全的, 在多執行緒環境中同時進行遍歷和修改操作可能會出現 ConcurrentModificationException
;
可以對每個操作都進行同步以解決這個問題, 但對於大部分操作是讀取資料的集合進行同步可能會使效能急劇下降, 在這種情況下這種效能損失是沒有必要的;
舉個例子, 以下的座標中, x 軸表示時間軸, y 軸表示不同的執行緒, +
表示讀取操作, *
表示修改操作:
| ++ ++ ++ ++ ++ ++ ++|++ + ++ + ++ + +++ + +| ++ ++ * ++ +* ++ +| ++ + + * ++ + ++ + |+ + +++ * + + +++ + ++| + + + + + + + + ++---------------------- 1 2 3複製程式碼
其中除了 1 2 3 這三個時刻之外, 其他時間都是隻有讀取操作, 在除了 1 2 3 之外的時間進行同步就是沒有必要的;
因此我們希望使用一種技術來處理那些在多執行緒環境下 “讀取操作” 遠遠多於 “修改操作” 的資源, 使得不需要通過對每一個操作都進行同步;
Solution
Java 類庫中提供了兩個 Copy-on-Write 的類: CopyOnWriteArrayList
和 CopyOnWriteArraySet
, 分別實現了 List
和 Set
兩個介面;
How CopyOnWriteArrayList Works
CopyOnWriteArrayList
只有在對其進行修改操作時才會進行同步操作, 因此其 add
, remove
等方法中均使用了同步機制;
在 CopyOnWriteArrayList
中, 定義了一個可重入鎖:
final transient ReentrantLock lock = new ReentrantLock();
複製程式碼
該鎖用於對所有修改集合的方法 (add
, remove
等) 進行同步, 在進行實際修改操作時, 會先複製原來的陣列, 再進行修改, 最後替換原來的:
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();
}
}複製程式碼
由於在修改時複製了一份資料, 因此所有讀取操作都無需進行同步:
public E get(int index) {
return get(getArray(), index);
}複製程式碼
但也會因此引入 “弱一致性” 問題;
所謂 “弱一致性” 是指當一個執行緒正在讀取資料時, 若此時有另一個執行緒同時在修改該區域的資料, 讀取的執行緒將無法讀取最新的資料, 即該讀取執行緒只能讀取到它讀取時刻以前的最新資料;
“弱一致性” 的另一個體現是當使用迭代器的時候, 使用迭代器遍歷集合時, 該迭代器只能遍歷到建立該迭代器時的資料, 對於建立了迭代器後對集合進行的修改, 該迭代器無法感知;
這是因為建立迭代器時, 迭代器對原始資料建立了一份 “快照 (Snapshot)”;
因此 CopyOnWriteArrayList
和 CopyOnWriteArraySet
只能適用於對資料實時性要求不高的場景;
How CopyOnWriteArraySet Works
CopyOnWriteArraySet
的實現是基於 CopyOnWriteArrayList
的, 其內部維護了一個 CopyOnWriteArrayList
例項 al
:
private final CopyOnWriteArrayList<
E>
al;
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList<
E>
();
}複製程式碼
所有對 CopyOnWriteArraySet
的操作都被委託給 al
, 如 add
方法:
public boolean add(E e) {
return al.addIfAbsent(e);
}複製程式碼
是非常典型的 組合模式 應用;