本篇文章是【Java集合系列】文章的第三篇侦啸,本系列將會(huì)逐個(gè)分析 Java 中的常用集合的特性及實(shí)現(xiàn)昭灵,然后對(duì)比不同場(chǎng)景下應(yīng)該選擇哪種集合使用偎痛。
List 系列
- Java 中的 ArrayList
- Java 中的 LinkedList
- Java 中的 CopyOnWriteArrayList
CopyOnWriteArrayList
先看看百科上關(guān)于 COW 的介紹:
寫入時(shí)復(fù)制(英語:Copy-on-write衅鹿,簡(jiǎn)稱COW)是一種計(jì)算機(jī)程序設(shè)計(jì)領(lǐng)域的優(yōu)化策略愕够。其核心思想是,如果有多個(gè)調(diào)用者(callers)同時(shí)請(qǐng)求相同資源(如內(nèi)存或磁盤上的數(shù)據(jù)存儲(chǔ))浪讳,他們會(huì)共同獲取相同的指針指向相同的資源缰盏,直到某個(gè)調(diào)用者試圖修改資源的內(nèi)容時(shí),系統(tǒng)才會(huì)真正復(fù)制一份專用副本(private copy)給該調(diào)用者,而其他調(diào)用者所見到的最初的資源仍然保持不變口猜。這過程對(duì)其他的調(diào)用者都是透明的(transparently)负溪。此作法主要的優(yōu)點(diǎn)是如果調(diào)用者沒有修改該資源,就不會(huì)有副本(private copy)被創(chuàng)建济炎,因此多個(gè)調(diào)用者只是讀取操作時(shí)可以共享同一份資源川抡。
簡(jiǎn)單來說,就是讀取時(shí)直接讀取不用加鎖同步须尚,寫入數(shù)據(jù)時(shí)會(huì)復(fù)制一份副本崖堤,然后將新的數(shù)據(jù)寫入到副本中,然后再把副本替換成原來的數(shù)據(jù)耐床。
因此 CopyOnWriteArrayList 是線程安全的密幔,另外也允許 null 元素。
這種方式導(dǎo)致了讀取速度很快撩轰,寫入速度較慢胯甩,適合多線程環(huán)境中經(jīng)常讀取但寫入很少的場(chǎng)景。
所以如果有個(gè)場(chǎng)景需要在多線程環(huán)境中使用堪嫂,頻繁讀取缸棵,但寫入次頻率很低资柔,例如黑名單白名單丛晌,每天更新一次签餐,那就可以使用 CopyOnWriteArrayList 來實(shí)現(xiàn)。
其中的迭代器是不允許對(duì)數(shù)據(jù)修改的愚战,調(diào)用remove
,set
,add
這些方法會(huì)直接拋異常娇唯。
一般可以按照常規(guī)的 List 來使用,其中沒有什么特殊的方法寂玲,就直接開始看源碼吧塔插。
CopyOnWriteArrayList 下面簡(jiǎn)稱 COWList。
源碼
我們先看其中兩個(gè)最重要的方法:
private transient volatile Object[] array;
final Object[] getArray() {
return array;
}
final void setArray(Object[] a) {
array = a;
}
上面定義的array
變量就是 COWList 中存儲(chǔ)數(shù)據(jù)的地方拓哟。
所有的讀取寫入操作都是通過對(duì)這個(gè)數(shù)組的操作想许。
因此構(gòu)造函數(shù)也是創(chuàng)建一個(gè)空的數(shù)組然后調(diào)用setArray
方法設(shè)置數(shù)組。
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
COWList 的優(yōu)勢(shì)之一就是即使在多線程環(huán)境中断序,讀取也是不需要加鎖的流纹,所以效率很高,那么我們來看看讀取方法是如何實(shí)現(xiàn)的:
public E get(int index) {
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}
可以看到這里直接通過索引讀取了數(shù)組的值违诗,沒有加鎖漱凝。這根我們使用Collections.synchronizedList
方法獲取到的SynchronizedList
效率要高。
可以看下SynchronizedList
是如何實(shí)現(xiàn)讀取及寫入的:
//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);
}
}
對(duì)比一下顯而易見诸迟,讀取不加鎖的話效率就高多了茸炒。
那么 COWList 是如何實(shí)現(xiàn)多線程環(huán)境下的安全問題的呢愕乎,看下寫入方法就明白了。
public boolean add(E e) {
synchronized (lock) {
Object[] elements = getArray();
int len = elements.length;
//復(fù)制數(shù)組
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
}
}
可以看到添加元素會(huì)加鎖壁公,加鎖之后會(huì)將原數(shù)組復(fù)制到一個(gè)更大(len + 1
)的數(shù)組中去感论,并將追加元素添加到數(shù)組末尾,完成操作后調(diào)用setArray
方法設(shè)置新數(shù)組紊册。
所以寫入的效率是很低的笛粘,需要進(jìn)行一次數(shù)組復(fù)制操作,用 COWList 時(shí)應(yīng)該盡量降低寫入操作的頻率湿硝,如果需要經(jīng)常寫入,那說明可能不適合用這個(gè)润努。
歡迎關(guān)注我的公眾號(hào)关斜,還有更多干貨。
微信掃描二維碼或者搜索:zhangke_blog