1、什麼是COW
維基百科定義:
寫入時複製(英語:Copy-on-write,簡稱COW)是一種計算機程序設計領域的優化策略。其核心思想是,如果有多個調用者(callers)同時請求相同資源(如內存或磁盤上的數據存儲),他們會共同獲取相同的指針指向相同的資源,直到某個調用者試圖修改資源的內容時,系統纔會真正複製一份專用副本(private copy)給該調用者,而其他調用者所見到的最初的資源仍然保持不變。這過程對其他的調用者都是透明的(transparently)。
此作法主要的優點是如果調用者沒有修改該資源,就不會有副本(private copy)被創建,因此多個調用者只是讀取操作時可以共享同一份資源。
大白話:
通俗的講就是,cow基於“寫時複製”的思想,當併發請求讀取相同的數據資源時,讀取的是同一份數據,當某個請求嘗試去修改資源時,就會通過複製出一個副本進而去修改副本的數據,其他請求讀取的數據還是最初不變的,最後將指向源數據的引用指向此線程修改後的副本數據。
2、Java中的Cow容器
Java中的Cow容器有兩個,在Jdk1.5版本中開始出現的,分別是CopyOnWriteArrayList及CopyOnWriteArraySet。
CopyOnWriteArrayList與CopyOnWriteArraySet基本一致,主要區別是在add方法,CopyOnWriteArraySet,有set的特性,即存儲元素的是不重複的,因此CopyOnWriteArraySet的add方法中使用的是addIfAbsent(E e),即只有當元素不存在的時候,纔會將元素添加到集合的尾部。
3、CopyOnWriteArrayList源碼分析
從源碼中,可以看出CopyOnWriteArrayList內部持有一個ReentrantLock鎖,最重要屬性array是一個Object類型的數組並且有volatile關鍵字修飾,而且這個array只能通過getArray()和setArray()來進行訪問。
/** The lock protecting all mutators */
final transient ReentrantLock lock = new ReentrantLock();
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
/**
* 獲取源數組
*/
final Object[] getArray() {
return array;
}
/**
*將源數組引用指向新數組
*/
final void setArray(Object[] a) {
array = a;
}
add()方法:
public boolean add(E e) {
final ReentrantLock lock = this.lock;
// 1、加鎖
lock.lock();
try {
// 2、獲取源數組引用
Object[] elements = getArray();
int len = elements.length;
// 3、拷貝出一個新數組,新數組長度=源數組長度 + 1
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 4、將元素添加到新數組的尾部
newElements[len] = e;
// 5、將源數組引用指向新數組
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
add()方法主要工作流程如下:
- 先進行加鎖操作
- 通過getArray()方法獲取源數組
- 拷貝出一個新數組,並且新數組的長度加1
- 將要添加的元素追加到新數組的尾部
- 通過setArray()方法將源數組引用指向新數組
- 最後釋放鎖
remove()方法
public boolean remove(Object o) {
// 1、獲取數組快照
Object[] snapshot = getArray();
// 2、獲取要移除元素的索引下標
int index = indexOf(o, snapshot, 0, snapshot.length);
// 3、若未找到,則直接返回false,否則調用remove方法進行移除
return (index < 0) ? false : remove(o, snapshot, index);
}
private static int indexOf(Object o, Object[] elements,
int index, int fence) {
// 若要移除的元素爲null,則直接遍歷數組,找到第一個值爲null的數組下標返回
if (o == null) {
for (int i = index; i < fence; i++)
if (elements[i] == null)
return i;
} else {
// 遍歷數組,通過equals方法找到要移除的元素的下標返回
for (int i = index; i < fence; i++)
if (o.equals(elements[i]))
return i;
}
// 未找到要刪除的元素,默認返回-1
return -1;
}
private boolean remove(Object o, Object[] snapshot, int index) {
final ReentrantLock lock = this.lock;
// 1、加鎖
lock.lock();
try {
// 2、獲取源數組
Object[] current = getArray();
int len = current.length;
// 3、若快照與當前數組不等,則說明併發情況下源數組已經被改變
if (snapshot != current) findIndex: {
// 取較小數組長度
int prefix = Math.min(index, len);
// 遍歷判斷是否能能找到要刪除的元素的下標,若找到則跳出if語句
for (int i = 0; i < prefix; i++) {
if (current[i] != snapshot[i] && eq(o, current[i])) {
index = i;
break findIndex;
}
}
// 若之前定位的數組下標大於當前源數組長度,則直接返回false
if (index >= len)
return false;
// 若源數組中索引下標位置的元素與要刪除的元素相等,則跳出if語句
if (current[index] == o)
break findIndex;
// 遍歷獲取要刪除的元素下標
index = indexOf(o, current, index, len);
if (index < 0)
return false;
}
// 創建新數組
Object[] newElements = new Object[len - 1];
// 將當前數組中索引下標位置之前的元素先拷貝到新數組中
System.arraycopy(current, 0, newElements, 0, index);
// 將當前數組中索引下標位置止嘔的元素再拷貝到新數組中
System.arraycopy(current, index + 1,
newElements, index,
len - index - 1);
// 通過setArray()方法將源數組引用指向新數組
setArray(newElements);
return true;
} finally {
// 解鎖
lock.unlock();
}
}
remove()方法的工作流程其實也不復雜,順着源碼往下看就能理順,主要流程如下:
- 先在無鎖的情況下,在當前數組中尋找要移除的元素下標,若未找到,則直接返回,否則調用重寫的remove()方法
- 調用重寫的remove()方法,會先進行加鎖操作
- 然後看當前數組在加鎖前是否已經發生變化(和未加鎖時獲取的源數組進行比較),因爲併發情況下,源數組可能已經被其他修改操作修改而發生變化。
- 若當前數組發生變化,則嘗試在獲取要刪除的元素的下標
- 找到要移除的元素下標,則生成新數組,並進行數組元素拷貝;否則直接返回
- 最後將源數組引用指向新數組
- 最後再釋放鎖
4、COW容器優缺點及適用場景
從源碼中我們就能體會到,cow容器提供了在修改操作時,採用複製新數組的方式,並在修改操作(添加或刪除)中加鎖,讀取操作在併發情況下並不能保證讀取的元素是最新的,但是修改操作會保證數據的最終一致性。
優點:
- 在多線程併發場景中,以犧牲空間來換取數據被併發修改的最終一致性,尤其適合讀多寫少的場景
- 併發修改操作(添加或刪除)不會出現併發修改異常(ConcurrentModificationException)
缺點:
- 不適合寫操作比較多的場景,寫操作由於加鎖,會影響性能
- 修改操作,會額外佔用一倍的內存空間
適用場景:
- 多線程併發情況下,且讀多寫少的場景,容忍犧牲一部分空間來換取多線程環境下的數據的最終一致性