寫入時(shí)復(fù)制(CopyOnWrite)思想
寫入時(shí)復(fù)制(CopyOnWrite,簡(jiǎn)稱COW)思想是計(jì)算機(jī)程序設(shè)計(jì)領(lǐng)域中的一種優(yōu)化策略。其核心思想是,如果有多個(gè)調(diào)用者(Callers)同時(shí)要求相同的資源(如內(nèi)存或者是磁盤上的數(shù)據(jù)存儲(chǔ)),他們會(huì)共同獲取相同的指針指向相同的資源,直到某個(gè)調(diào)用者視圖修改資源內(nèi)容時(shí),系統(tǒng)才會(huì)真正復(fù)制一份專用副本(private copy)給該調(diào)用者,而其他調(diào)用者所見(jiàn)到的最初的資源仍然保持不變。這過(guò)程對(duì)其他的調(diào)用者都是透明的(transparently)。此做法主要的優(yōu)點(diǎn)是如果調(diào)用者沒(méi)有修改資源,就不會(huì)有副本(private copy)被創(chuàng)建,因此多個(gè)調(diào)用者只是讀取操作時(shí)可以共享同一份資源。
CopyOnWriteArrayList
CopyOnWriteArrayList是Java中的并發(fā)容器類,同時(shí)也是符合寫入時(shí)復(fù)制思想的CopyOnWrite容器。關(guān)于CopyOnWriteArrayList的介紹我就不過(guò)多贅述了,可以參考我這篇博客來(lái)了解-----《Java并發(fā)編程實(shí)戰(zhàn)》學(xué)習(xí)筆記--并發(fā)容器類。
下面將通過(guò)CopyOnWriteArrayList的源碼來(lái)了解寫入時(shí)復(fù)制思想

CopyOnWriteArrayList中有一個(gè)ReentrantLock鎖,這是一個(gè)可重入的鎖,提供了類似于synchronized的功能和內(nèi)存語(yǔ)義,但是ReentrantLock的功能性更為全面。由于本文重點(diǎn)是介紹CopyOnWrite思想,所以對(duì)于ReentrantLock就不過(guò)多介紹,只要知道它是用來(lái)保證線程安全性的即可。

下面這個(gè)兩個(gè)方法是CopyOnWriteArrayList實(shí)現(xiàn)寫入時(shí)復(fù)制的關(guān)鍵:
一個(gè)是獲得當(dāng)前容器數(shù)組的一個(gè)副本,另一個(gè)是將容器數(shù)組的引用指向一個(gè)修改之后的數(shù)組。


下面來(lái)看看使用了寫入時(shí)復(fù)制的set方法:
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();//獲得鎖
try {
Object[] elements = getArray();//得到目前容器數(shù)組的一個(gè)副本
E oldValue = get(elements, index);//獲得index位置對(duì)應(yīng)元素目前的值
if (oldValue != element) {
int len = elements.length;
//創(chuàng)建一個(gè)新的數(shù)組newElements,將elements復(fù)制過(guò)去
Object[] newElements = Arrays.copyOf(elements, len);
//將新數(shù)組中index位置的元素替換為element
newElements[index] = element;
//這一步是關(guān)鍵,作用是將容器中array的引用指向修改之后的數(shù)組,即newElements
setArray(newElements);
} else {
//index位置元素的值與element相等,故不對(duì)容器數(shù)組進(jìn)行修改
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();//解除鎖定
}
}
我們可以看到,在set方法中,我們首先是獲得了當(dāng)前數(shù)組的一個(gè)拷貝獲得一個(gè)新的數(shù)組,然后在這個(gè)新的數(shù)組上完成我們想要的操作。當(dāng)操作完成之后,再把原有數(shù)組的引用指向新的數(shù)組。并且在此過(guò)程中,我們只擁有一個(gè)事實(shí)不可變對(duì)象,即容器中的array。這樣一來(lái)就很巧妙地體現(xiàn)了CopyOnWrite思想。
其實(shí)這也是讀寫分離的一種體現(xiàn)。當(dāng)線程在對(duì)線程進(jìn)行讀或者寫的操作時(shí),其實(shí)操作的是不同的容器。這么一來(lái)我們可以對(duì)容器進(jìn)行并發(fā)的讀,而不需要加鎖。實(shí)際上就是這么做的:

那么問(wèn)題來(lái)了
- 如果每次都要對(duì)原有的容器進(jìn)行復(fù)制,豈不是很消耗內(nèi)存?
- 還有,假如說(shuō)一個(gè)線程正在對(duì)容器進(jìn)行修改,另一個(gè)線程正在讀取容器的內(nèi)容,這其實(shí)是兩個(gè)容器數(shù)組。那么讀線程讀到的不是舊數(shù)據(jù)嗎?
沒(méi)錯(cuò),這正是CopyOnWrite容器t的不足:
- 存在內(nèi)存占用的問(wèn)題,因?yàn)槊看螌?duì)容器結(jié)構(gòu)進(jìn)行修改的時(shí)候都要對(duì)容器進(jìn)行復(fù)制,這么一來(lái)我們就有了舊有對(duì)象和新入的對(duì)象,會(huì)占用兩份內(nèi)存。如果對(duì)象占用的內(nèi)存較大,就會(huì)引發(fā)頻繁的垃圾回收行為,降低性能;
- CopyOnWrite只能保證數(shù)據(jù)最終的一致性,不能保證數(shù)據(jù)的實(shí)時(shí)一致性。
所以對(duì)于CopyOnWrite容器來(lái)說(shuō),只適合在讀操作遠(yuǎn)遠(yuǎn)多于寫操作的場(chǎng)景下使用,比如說(shuō)緩存。