談?wù)剬懭霑r(shí)復(fù)制的思想---以CopyOnWriteArrayList為例

寫入時(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ù)制思想

ReentrantLock鎖

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

容器自身的數(shù)組,僅當(dāng)使用getArray/setArray方法時(shí)才能獲得

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

獲得容器數(shù)組

將容器數(shù)組的引用指向a

下面來(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í)際上就是這么做的:

沒(méi)有加鎖的讀操作

那么問(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ō)緩存。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容