深入解析 CopyOnWriteArrayList
在多線程編程中,確保數據結構的安全性和高效性是一個重要的挑戰。Java 提供了多種并發工具和數據結構來幫助開發者應對這一挑戰。其中,CopyOnWriteArrayList 是一個非常有用且高效的線程安全列表實現,所以本文將從案例實踐和源碼剖析的角度深度解讀CopyOnWriteArrayList,希望對你有幫助。

一、詳解java中有序集合的并發容器
1. Vector如何實現線程安全
對于并發操作的有序集合容器,相信大部分都會想到非常傳統的容器Vector,原因很簡單,查看源碼時我們非常直觀的看到其針對任何讀寫操作都上了一把synchronized 鎖:
public synchronized E get(int index) {
//......
//獲取對象實例鎖之后,調用elementData返回元素
return elementData(index);
}
public synchronized E set(int index, E element) {
//......
//獲取實例鎖后開始執行更新操作,先獲取舊元素
E oldValue = elementData(index);
//更新元素
elementData[index] = element;
//返回舊的值
return oldValue;
}2. synchronizedList如何保證線程安全
Collections.synchronizedList同理,只不過synchronizedList這個方法是針對原生數組的封裝,通過方法內部上一把對象鎖來保證線程安全:
public E get(int index) {
synchronized (mutex) {return list.get(index);}
}
public E set(int index, E element) {
synchronized (mutex) {return list.set(index, element);}
}3. Vector和synchronizedList真的可以保證并發操作安全嗎?
盡管Vector和synchronizedList都通過加鎖的方式完成并發操作的互斥,但是他們真的安全嘛?如下代碼所示,在遍歷時進行集合清除操作,就會出現ConcurrentModificationException異常:
Vector<Integer> vector = new Vector<>();
vector.add(1);
vector.add(2);
vector.add(3);
vector.add(4);
vector.add(5);
//迭代期間一個并發線程清除元素
for (Integer item : vector) {
new Thread(vector::clear).start();
System.out.println(item);
}4. 為什么Vector加了synchronized之后在多線程操作下還會出現異常呢?
本質上這是一種fail-fast(快速失敗)思想,即針對可能發生的異常進行提前表明故障的一種工作機制,我們都知道util包下的集合默認情況下是不支持線程安全的,所以JDK設計者為了能夠提前感知并發操作失敗并拋出異常,提出通過檢查迭代期間修改次數是否變化來實現fail-fast,由此保證在避免在異常時執行非必要的復雜代碼。
在多線程情況下,線程1進行并發修改操作,不斷修改當前集合的modCount ,在這期間,另一個線程初始化一個迭代器進行遍歷,這是就會出現expectedModCount會初始化為線程1某個操作階段的modCount不等,進而觸發fail-fast告知用戶當前非線程安全容器存在線程安全問題,需要注意:

二、詳解cow思想
1. 什么是cow思想,如何保證的線程安全
從CopyOnWriteArrayList源碼中可知,COW即通過采用寫時復制的思想,在迭代時的修改通過復制一份快照數組,并基于該數組完成并發修改操作,完成操作后再原子替換調原來的數組,由此保證線程安全,因為該操作涉及寫時復制以及大數組的拷貝操作,這其中的開銷還是蠻大的,一般情況下的CopyOnWriteArrayList更適用于一些讀多寫少的并發場景:
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;
//array指向新的數組
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}2. 什么是fail-fast和fail-safe
關于fail-fast引用medium中一篇文章關于fail-fast和fail-safe的說法:
Fail-fast systems are designed to immediately stop functioning upon encountering an unexpected condition. This immediate failure helps to catch errors early, making debugging more straightforward.
快速失敗的思想即針對可能發生的異常進行提前表明故障并停止運行,通過盡早的發現和停止錯誤,降低故障系統級聯的風險。
我們都知道java.util包下的大部分集合是不支持線程安全的,所以JDK設計者為了能夠提前發現并發操作導致線程安全風險,提出通過維護一個modCount記錄修改的次數,迭代期間通過比對預期修改次數expectedModCount和modCount是否一致來判斷是否存在并發操作,從而實現快速失敗,由此保證在避免在異常時執行非必要的復雜代碼。
對應的我們給出下面這樣一段在示例,我們首先插入100個操作元素,一個線程迭代元素,一個線程刪除元素,最終輸出結果如愿拋出ConcurrentModificationException:
ArrayList<Integer> list = new ArrayList<>();
CountDownLatch countDownLatch = new CountDownLatch(2);
//添加幾個元素
for (int i = 0; i < 100; i++) {
list.add(i);
}
Thread t1 = new Thread(() -> {
//迭代元素
for (Integer i : list) {
i++;
}
countDownLatch.countDown();
});
Thread t2 = new Thread(() -> {
System.out.println("刪除元素1");
list.remove(1);
countDownLatch.countDown();
});
t1.start();
t2.start();
countDownLatch.await();我們在初始化時插入了100個元素,此時對應的修改modCount次數為100,隨后線程2在線程1迭代期間進行元素刪除操作,此時對應的modCount就變為101。 線程1在隨后foreach第2輪循環發現modCount 為101,與預期的expectedModCount(值為100因為初始化插入了元素100個)不等,判定為并發操作異常,于是便快速失敗,拋出ConcurrentModificationException:

對此我們也給出迭代器獲取下一個元素時的next方法,可以看到其內部的checkForComodification具有針對修改次數比對的邏輯:
public E next() {
//檢查是否存在并發修改
checkForComodification();
//......
//返回下一個元素
return (E) elementData[lastRet = i];
}
final void checkForComodification() {
//當前循環遍歷次數和預期修改次數不一致時,就會拋出ConcurrentModificationException
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}而fail-safe也就是安全失敗的含義,它旨在即使面對意外情況也能恢復并繼續運行,這使得它特別適用于不確定或者不穩定的環境:
Fail-safe systems take a different approach, aiming to recover and continue even in the face of unexpected conditions. This makes them particularly suited for uncertain or volatile environments.
該思想常運用于并發容器,最經典的實現就是CopyOnWriteArrayList的實現,通過寫時復制的思想保證在進行修改操作時復制出一份快照,基于這份快照完成添加或者刪除操作后,將CopyOnWriteArrayList底層的數組引用指向這個新的數組空間,由此避免迭代時被并發修改所干擾導致線程安全問題,當然這種做法也使得進行遍歷操作時無法獲得實時結果:

對應我們也給出CopyOnWriteArrayList實現fail-safe的核心代碼,可以看到它的實現就是通過getArray獲取數組引用然后通過Arrays.copyOf得到一個數組的快照,基于這個快照完成添加操作后,修改底層array變量指向的引用地址由此完成寫時復制:
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;
//array指向新的數組
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}3. 與傳統集合的性能比對
與傳統集合相比,CopyOnWriteArrayList更適合讀多寫少的情況,例如:黑名單、配置等相關集合。如下代碼所示,我們就能看出寫操作CopyOnWriteArrayList確實開銷更大。且CopyOnWrite容器只能保證數據的最終一致性,不能保證數據的實時一致性:
long start = System.currentTimeMillis();
List<Integer> copyOnWriteArrayList = new CopyOnWriteArrayList<>();
int loopCount = 10_0000;
//添加10w個元素到copyOnWriteArrayList
for (int i = 0; i < loopCount; i++) {
copyOnWriteArrayList.add(1);
}
long end = System.currentTimeMillis();
System.out.println(end - start);
//添加10w個元素到synchronizedList
start = System.currentTimeMillis();
List<Integer> synchronizedList = Collections.synchronizedList(new ArrayList<>());
for (int i = 0; i < loopCount; i++) {
synchronizedList.add(1);
}
end = System.currentTimeMillis();
System.out.println(end - start);輸出結果:
3813
4






















