Copy-On-Write簡稱COW腹缩,是一種用于程序設(shè)計中的優(yōu)化策略。其基本思路是空扎,從一開始大家都在共享同一個內(nèi)容藏鹊,當(dāng)某個人想要修改這個內(nèi)容的時候,才會真正把內(nèi)容Copy出去形成一個新的內(nèi)容然后再改转锈,這是一種延時懶惰策略盘寡。從JDK1.5開始Java并發(fā)包里提供了兩個使用CopyOnWrite機(jī)制實(shí)現(xiàn)的并發(fā)容器,它們是CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWrite容器非常有用撮慨,可以在非常多的并發(fā)場景中使用到竿痰。
(一)什么是CopyOnWrite容器脆粥?
CopyOnWrite容器即寫時復(fù)制的容器。通俗的理解是當(dāng)我們往一個容器添加元素的時候影涉,不直接往當(dāng)前容器添加变隔,而是先將當(dāng)前容器進(jìn)行Copy,復(fù)制出一個新的容器蟹倾,然后新的容器里添加元素匣缘,添加完元素之后,再將原容器的引用指向新的容器鲜棠。這樣做的好處是我們可以對CopyOnWrite容器進(jìn)行并發(fā)的讀肌厨,而不需要加鎖,因?yàn)楫?dāng)前容器不會添加任何元素豁陆。所以CopyOnWrite容器也是一種讀寫分離的思想柑爸,讀和寫不同的容器。
(二)CopyOnWriteArrayList如何做到線程安全的盒音?
CopyOnWriteArrayList使用了一種叫寫時復(fù)制的方法表鳍,當(dāng)有新元素添加到CopyOnWriteArrayList時,先從原有的數(shù)組中拷貝一份出來里逆,然后在新的數(shù)組做寫操作进胯,寫完之后,再將原來的數(shù)組引用指向到新數(shù)組原押。
當(dāng)有新元素加入的時候,如下圖偎血,創(chuàng)建新數(shù)組诸衔,并往新數(shù)組中加入一個新元素,這個時候,array這個引用仍然是指向原數(shù)組的颇玷。
當(dāng)元素在新數(shù)組添加成功后笨农,將array這個引用指向新數(shù)組。
(三)CopyOnWriteArrayList源碼分析
1.繼承與接口的關(guān)系關(guān)系帖渠,由此我們可以看出CopyOnWriteArrayList其實(shí)和一般的List集合實(shí)現(xiàn)類一樣都實(shí)現(xiàn)了基本的接口谒亦。
public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, Serializable
2.屬性字段
private transient volatile Object[] elements;
volatile原理:
Java語言提供了一種稍弱的同步機(jī)制,即volatile變量空郊,用來確保將變量的更新操作通知到其他線程份招。當(dāng)把變量聲明為volatile類型后,編譯器與運(yùn)行時都會注意到這個變量是共享的狞甚,因此不會將該變量上的操作與其他內(nèi)存操作一起重排序锁摔。volatile變量不會被緩存在寄存器或者對其他處理器不可見的地方,因此在讀取volatile類型的變量時總會返回最新寫入的值哼审。
在訪問volatile變量時不會執(zhí)行加鎖操作谐腰,因此也就不會使執(zhí)行線程阻塞孕豹,因此volatile變量是一種比sychronized關(guān)鍵字更輕量級的同步機(jī)制。
當(dāng)對非 volatile 變量進(jìn)行讀寫的時候十气,每個線程先從內(nèi)存拷貝變量到CPU緩存中励背。如果計算機(jī)有多個CPU,每個線程可能在不同的CPU上被處理砸西,這意味著每個線程可以拷貝到不同的 CPU cache 中叶眉。而聲明變量是 volatile 的,JVM 保證了每次讀變量都從內(nèi)存中讀籍胯,跳過 CPU cache 這一步竟闪。
volatile 性能:
volatile 的讀性能消耗與普通變量幾乎相同,但是寫操作稍慢杖狼,因?yàn)樗枰诒镜卮a中插入許多內(nèi)存屏障指令來保證處理器不發(fā)生亂序執(zhí)行炼蛤。
3.構(gòu)造函數(shù)
public CopyOnWriteArrayList() {
elements = EmptyArray.OBJECT; //創(chuàng)建一個Object類型的空表
}
//將集合c復(fù)制到CopyOnWriteArrayList中
@SuppressWarnings("unchecked")
public CopyOnWriteArrayList(Collection<? extends E> collection) {
this((E[]) collection.toArray());
}
//將傳入的數(shù)組復(fù)制到CopyOnWriteArrayList中
public CopyOnWriteArrayList(E[] array) {
this.elements = Arrays.copyOf(array, array.length, Object[].class);
}
4.增刪改查操作
(1)添加操作
public synchronized boolean add(E e) {
Object[] newElements = new Object[elements.length + 1]; //這里是重新構(gòu)造了一個新的數(shù)組
System.arraycopy(elements, 0, newElements, 0, elements.length);
newElements[elements.length] = e;
elements = newElements; //然后再把新的數(shù)組復(fù)制給原來的數(shù)組
return true;
}
CopyOnWriteArrayList的整個add操作都是在鎖的保護(hù)下進(jìn)行的。 這樣做是為了避免在多線程并發(fā)add的時候蝶涩,復(fù)制出多個副本出來,把數(shù)據(jù)搞亂了理朋。下面的幾個add方法也是同理。
public synchronized void add(int index, E e) {
Object[] newElements = new Object[elements.length + 1];
System.arraycopy(elements, 0, newElements, 0, index);
newElements[index] = e;
System.arraycopy(elements, index, newElements, index + 1, elements.length - index);
elements = newElements;
}
public synchronized boolean addAll(Collection<? extends E> collection) {
return addAll(elements.length, collection);
}
public synchronized boolean addAll(int index, Collection<? extends E> collection) {
Object[] toAdd = collection.toArray();
Object[] newElements = new Object[elements.length + toAdd.length];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(toAdd, 0, newElements, index, toAdd.length);
System.arraycopy(elements, index,
newElements, index + toAdd.length, elements.length - index);
elements = newElements;
return toAdd.length > 0;
}
由于所有的寫操作都是在新數(shù)組進(jìn)行的绿聘,這個時候如果有線程并發(fā)的寫嗽上,則通過鎖來控制,如果有線程并發(fā)的讀熄攘,則分幾種情況:
1兽愤、如果寫操作未完成,那么直接讀取原數(shù)組的數(shù)據(jù)挪圾;
2浅萧、如果寫操作完成,但是引用還未指向新數(shù)組哲思,那么也是讀取原數(shù)組數(shù)據(jù)洼畅;
3、如果寫操作完成棚赔,并且引用已經(jīng)指向了新的數(shù)組帝簇,那么直接從新數(shù)組中讀取數(shù)據(jù)。
可見靠益,CopyOnWriteArrayList的讀操作是可以不用加鎖的丧肴。
(2)刪除操作,也是開辟另一個數(shù)組來操作
public synchronized E remove(int index) {
@SuppressWarnings("unchecked")
E removed = (E) elements[index];
removeRange(index, index + 1);
return removed;
}
private void removeRange(int from, int to) {
Object[] newElements = new Object[elements.length - (to - from)];
System.arraycopy(elements, 0, newElements, 0, from);
System.arraycopy(elements, to, newElements, from, elements.length - to);
elements = newElements;
}
public synchronized boolean remove(Object o) {
int index = indexOf(o);
if (index == -1) {
return false;
}
remove(index);
return true;
}
public synchronized boolean removeAll(Collection<?> collection) {
return removeOrRetain(collection, false, 0, elements.length) != 0;
}
public synchronized boolean retainAll(Collection<?> collection) {
return removeOrRetain(collection, true, 0, elements.length) != 0;
}
(3)修改操作
public synchronized E set(int index, E e) {
Object[] newElements = elements.clone();
@SuppressWarnings("unchecked")
E result = (E) newElements[index];
newElements[index] = e;
elements = newElements;
return result;
}
(4)讀取操作
@SuppressWarnings("unchecked")
public E get(int index) {
return (E) elements[index];
}
讀的時候不需要加鎖捆毫,如果讀的時候有多個線程正在向CopyOnWriteArrayList添加數(shù)據(jù)闪湾,讀還是會讀到舊的數(shù)據(jù),因?yàn)閷懙臅r候不會鎖住舊的CopyOnWriteArrayList绩卤。
(5)其他的集合方法基本和ArrayList一樣了途样,就不在繼續(xù)分析了江醇。
通過上面的分析,CopyOnWriteArrayList 有幾個缺點(diǎn):
1何暇、由于寫操作的時候陶夜,存在內(nèi)存占用的問題,因?yàn)槊看螌θ萜鹘Y(jié)構(gòu)進(jìn)行修改的時候都要對容器進(jìn)行復(fù)制裆站,這么一來我們就有了舊有對象和新入的對象条辟,會占用兩份內(nèi)存。如果對象占用的內(nèi)存較大宏胯,就會引發(fā)頻繁的垃圾回收行為羽嫡,降低性能;
2肩袍、不能用于實(shí)時讀的場景杭棵,像拷貝數(shù)組、新增元素都需要時間氛赐,所以調(diào)用一個set操作后魂爪,讀取到數(shù)據(jù)可能還是舊的,雖然CopyOnWriteArrayList 能做到最終一致性,但是還是沒法滿足實(shí)時性要求;
CopyOnWriteArrayList 合適讀多寫少的場景艰管,不過這類慎用滓侍。因?yàn)檎l也沒法保證CopyOnWriteArrayList 到底要放置多少數(shù)據(jù),萬一數(shù)據(jù)稍微有點(diǎn)多牲芋,每次add/set都要重新復(fù)制數(shù)組撩笆,這個代價實(shí)在太高昂了。在高性能的互聯(lián)網(wǎng)應(yīng)用中缸浦,這種操作分分鐘引起故障浇衬。
CopyOnWriteArrayList透露的思想,如上面的分析CopyOnWriteArrayList表達(dá)的一些思想:
1餐济、讀寫分離,讀和寫分開
2胆剧、最終一致性 (CopyOnWrite只能保證數(shù)據(jù)最終的一致性絮姆,不能保證數(shù)據(jù)的實(shí)時一致性。)
3秩霍、使用另外開辟空間的思路篙悯,來解決并發(fā)沖突.
CopyOnWriteArrayList對我們還說還是了解有這么一個東西存在而已,一般Android都不會使用铃绒。