CopyOnWriteArrayList的原理與應(yīng)用

?Copy-On-Write簡稱COW辉巡,是一種用于程序設(shè)計(jì)中的優(yōu)化策略太雨。其基本思路是,從一開始大家都在共享同一個(gè)內(nèi)容茫蛹,當(dāng)某個(gè)人想要修改這個(gè)內(nèi)容的時(shí)候操刀,才會(huì)真正把內(nèi)容Copy出去形成一個(gè)新的內(nèi)容然后再改,這是一種延時(shí)懶惰策略婴洼。從JDK1.5開始Java并發(fā)包里提供了兩個(gè)使用CopyOnWrite機(jī)制實(shí)現(xiàn)的并發(fā)容器,它們是CopyOnWriteArrayListCopyOnWriteArraySet骨坑。CopyOnWrite容器非常有用,可以在非常多的并發(fā)場景中使用到窃蹋。

一. 什么是CopyOnWrite容器

?CopyOnWrite容器即寫時(shí)復(fù)制的容器卡啰。通俗的理解是當(dāng)我們往一個(gè)容器添加元素的時(shí)候静稻,不直接往當(dāng)前容器添加,而是先將當(dāng)前容器進(jìn)行Copy匈辱,復(fù)制出一個(gè)新的容器振湾,然后新的容器里添加元素,添加完元素之后亡脸,再將原容器的引用指向新的容器押搪。這樣做的好處是我們可以對CopyOnWrite容器進(jìn)行并發(fā)的讀,而不需要加鎖浅碾,因?yàn)楫?dāng)前容器不會(huì)添加任何元素大州。所以CopyOnWrite容器也是一種讀寫分離的思想,讀和寫不同的容器垂谢。


二. 證明CopyOnWriteArrayList是線程安全的

ReadThread.java:從List中讀取數(shù)據(jù)的線程

import java.util.List;

/**
 * <Description> <br>
 *
 * @author Sunny<br>
 * @version 1.0<br>
 * @taskId: <br>
 * @createDate 2018/08/27 13:14 <br>
 * @see com.sunny.jdk.concurrent.cow.copyonwritearraylist <br>
 */
public class ReadThread implements Runnable {
    private List<Integer> list;

    public ReadThread(List<Integer> list) {
        this.list = list;
    }

    @Override
    public void run() {
        for (Integer ele : list) {
            System.out.println("ReadThread:"+ele);
        }
    }
}

WriteThread.java:向List中寫數(shù)據(jù)的線程厦画;

/**
 * <Description> <br>
 *
 * @author Sunny<br>
 * @version 1.0<br>
 * @taskId: <br>
 * @createDate 2018/08/27 13:14 <br>
 * @see com.sunny.jdk.concurrent.cow.copyonwritearraylist <br>
 */
public class WriteThread implements Runnable {
    private List<Integer> list;

    public WriteThread(List<Integer> list) {
        this.list = list;
    }

    @Override
    public void run() {
        Integer num = new Random().nextInt(10);
        this.list.add(num);
        System.out.println("Write Thread:" + num);
    }
}

TestCopyOnWriteArrayList .java:實(shí)現(xiàn)兩個(gè)方法,一個(gè)使用ArrayList容器滥朱,一個(gè)使用CopyOnWriteArrayList容器根暑,來進(jìn)行多線程的讀寫操作;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * <Description> 證明CopyOnWriteArrayList是線程安全的<br>
 *
 * @author Sunny<br>
 * @version 1.0<br>
 * @taskId: <br>
 * @createDate 2018/08/27 13:14 <br>
 * @see com.sunny.jdk.concurrent.cow.copyonwritearraylist <br>
 */
public class TestCopyOnWriteArrayList {
    private void testCopyOnWriteArrayList() {
        //1徙邻、初始化CopyOnWriteArrayList
        List<Integer> tempList = Arrays.asList(new Integer [] {1,2});
        CopyOnWriteArrayList<Integer> copyList = new CopyOnWriteArrayList<>(tempList);


        //2排嫌、模擬多線程對list進(jìn)行讀和寫
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        executorService.execute(new ReadThread(copyList));
        executorService.execute(new WriteThread(copyList));
        executorService.execute(new WriteThread(copyList));
        executorService.execute(new WriteThread(copyList));
        executorService.execute(new ReadThread(copyList));
        executorService.execute(new WriteThread(copyList));
        executorService.execute(new ReadThread(copyList));
        executorService.execute(new WriteThread(copyList));
        executorService.shutdown();

        System.out.println("copyList size:"+copyList.size());
    }
    private void testArrayList() {
        //1、初始化CopyOnWriteArrayList
        List<Integer> arrList = new ArrayList();
        arrList.add(1);
        arrList.add(2);


        //2缰犁、模擬多線程對list進(jìn)行讀和寫
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        executorService.execute(new ReadThread(arrList));
        executorService.execute(new WriteThread(arrList));
        executorService.execute(new WriteThread(arrList));
        executorService.execute(new WriteThread(arrList));
        executorService.execute(new ReadThread(arrList));
        executorService.execute(new WriteThread(arrList));
        executorService.execute(new ReadThread(arrList));
        executorService.execute(new WriteThread(arrList));
        executorService.shutdown();

        System.out.println("arrList size:"+ arrList.size());
    }


    public static void main(String[] args) {
        TestCopyOnWriteArrayList tcowal = new TestCopyOnWriteArrayList();
        //tcowal.testCopyOnWriteArrayList();
        tcowal.testArrayList();
    }
}

如果調(diào)用tcowal.testCopyOnWriteArrayList();方法淳地,則會(huì)打印如下:

ReadThread:1
ReadThread:2
copyList size:2
ReadThread:1
ReadThread:2
ReadThread:1
ReadThread:2
Write Thread:5
Write Thread:5
Write Thread:0
Write Thread:2
Write Thread:5

Process finished with exit code 0

如果調(diào)用tcowal.testArrayList();方法,則會(huì)打印如下:

ReadThread:1
ReadThread:2
arrList size:2
ReadThread:1
Write Thread:8
Write Thread:9
Write Thread:8
ReadThread:1
ReadThread:2
Write Thread:6
Write Thread:9
Exception in thread "pool-1-thread-5" Exception in thread "pool-1-thread-7" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
    at java.util.ArrayList$Itr.next(ArrayList.java:851)
    at com.sunny.jdk.concurrent.cow.copyonwritearraylist.ReadThread.run(ReadThread.java:23)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at java.lang.Thread.run(Thread.java:748)
java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
    at java.util.ArrayList$Itr.next(ArrayList.java:851)
    at com.sunny.jdk.concurrent.cow.copyonwritearraylist.ReadThread.run(ReadThread.java:23)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at java.lang.Thread.run(Thread.java:748)

Process finished with exit code 0

說明了CopyOnWriteArrayList并發(fā)多線程的環(huán)境下帅容,仍然能很好的工作颇象。


三. CopyOnWriteArrayList的實(shí)現(xiàn)原理

?現(xiàn)在我們來通過看源碼的方式來理解CopyOnWriteArrayList,實(shí)際上CopyOnWriteArrayList內(nèi)部維護(hù)的就是一個(gè)數(shù)組并徘,如下:

/** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array;

并且該數(shù)組引用是被volatile修飾夯到,注意這里僅僅是修飾的是數(shù)組引用,關(guān)于volatile很重要的一條性質(zhì)是它能夠夠保證可見性饮亏,對list來說,我們自然而然最關(guān)心的就是讀寫的時(shí)候阅爽,分別為get和add方法的實(shí)現(xiàn)路幸。

3.1 get方法實(shí)現(xiàn)原理

?get方法源碼如下:

    /**
     * Gets the array.  Non-private so as to also be accessible
     * from CopyOnWriteArraySet class.
     */
    final Object[] getArray() {
        return array;
    }

    @SuppressWarnings("unchecked")
    private E get(Object[] a, int index) {
        return (E) a[index];
    }

    /**
     * {@inheritDoc}
     *
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
        return get(getArray(), index);
    }

?可以看出來get方法實(shí)現(xiàn)非常簡單,幾乎就是一個(gè)單線程程序付翁,沒有對多線程添加任何的線程安全控制简肴,也沒有加鎖也沒有CAS操作等等,原因是百侧,所有的讀線程只是會(huì)讀取數(shù)據(jù)容器中的數(shù)據(jù)砰识,并不會(huì)進(jìn)行修改能扒。

3.1 add方法實(shí)現(xiàn)原理

add方法源碼如下:

    /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

    /**
     * Inserts the specified element at the specified position in this
     * list. Shifts the element currently at that position (if any) and
     * any subsequent elements to the right (adds one to their indices).
     *
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public void add(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            if (index > len || index < 0)
                throw new IndexOutOfBoundsException("Index: "+index+
                                                    ", Size: "+len);
            Object[] newElements;
            int numMoved = len - index;
            if (numMoved == 0)
                newElements = Arrays.copyOf(elements, len + 1);
            else {
                newElements = new Object[len + 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index, newElements, index + 1,
                                 numMoved);
            }
            newElements[index] = element;
            setArray(newElements);
        } finally {
            lock.unlock();
        }
    }

add方法的邏輯也比較容易理解,請看上面的注釋辫狼。需要注意這么幾點(diǎn):

  • 采用ReentrantLock初斑,保證同一時(shí)刻只有一個(gè)寫線程正在進(jìn)行數(shù)組的復(fù)制,否則的話內(nèi)存中會(huì)有多份被復(fù)制的數(shù)據(jù)膨处;
  • 前面說過數(shù)組引用是volatile修飾的见秤,因此將舊的數(shù)組引用指向新的數(shù)組,根據(jù)volatilehappens-before規(guī)則真椿,寫線程對數(shù)組引用的修改對讀線程是可見的鹃答。
  • 由于在寫數(shù)據(jù)的時(shí)候,是在新的數(shù)組中插入數(shù)據(jù)的突硝,從而保證讀寫是在兩個(gè)不同的數(shù)據(jù)容器中進(jìn)行操作测摔。

這里還有這樣一個(gè)問題: 為什么需要復(fù)制呢? 如果將array數(shù)組設(shè)定為volatile的解恰, 對volatile變量寫happens-before讀锋八,讀線程不是能夠感知到volatile變量的變化。

原因是修噪,這里volatile的修飾的僅僅只是數(shù)組引用查库,數(shù)組中的元素的修改是不能保證可見性的。因此COW采用的是新舊兩個(gè)數(shù)據(jù)容器黄琼,通過setArray(newElements);這一行代碼將數(shù)組引用指向新的數(shù)組樊销。


四. CopyOnWrite的使用場景

CopyOnWrite并發(fā)容器用于讀多寫少的并發(fā)場景。比如白名單脏款,黑名單围苫,商品類目的訪問和更新場景,假如我們有一個(gè)搜索網(wǎng)站撤师,用戶在這個(gè)網(wǎng)站的搜索框中剂府,輸入關(guān)鍵字搜索內(nèi)容,但是某些關(guān)鍵字不允許被搜索剃盾。這些不能被搜索的關(guān)鍵字會(huì)被放在一個(gè)黑名單當(dāng)中腺占,黑名單每天晚上更新一次。當(dāng)用戶搜索時(shí)痒谴,會(huì)檢查當(dāng)前關(guān)鍵字在不在黑名單當(dāng)中衰伯,如果在,則提示不能搜索积蔚。實(shí)現(xiàn)代碼如下:

import java.util.Map;

import com.ifeve.book.forkjoin.CopyOnWriteMap;

/**
 * 黑名單服務(wù)
 *
 *
 */
public class BlackListServiceImpl {

    private static CopyOnWriteMap<String, Boolean> blackListMap = new CopyOnWriteMap<String, Boolean>(
            1000);

    public static boolean isBlackList(String id) {
        return blackListMap.get(id) == null ? false : true;
    }

    public static void addBlackList(String id) {
        blackListMap.put(id, Boolean.TRUE);
    }

    /**
     * 批量添加黑名單
     *
     * @param ids
     */
    public static void addBlackList(Map<String,Boolean> ids) {
        blackListMap.putAll(ids);
    }

}

代碼很簡單意鲸,但是使用CopyOnWriteMap需要注意兩件事情:

  1. 減少擴(kuò)容開銷。根據(jù)實(shí)際需要,初始化CopyOnWriteMap的大小怎顾,避免寫時(shí)CopyOnWriteMap擴(kuò)容的開銷读慎。

  2. 使用批量添加。因?yàn)槊看翁砑踊蔽恚萜髅看味紩?huì)進(jìn)行復(fù)制夭委,所以減少添加次數(shù),可以減少容器的復(fù)制次數(shù)蚜退。如使用上面代碼里的addBlackList方法闰靴。


五. CopyOnWriteArrayList的缺點(diǎn)

?CopyOnWrite容器有很多優(yōu)點(diǎn),但是同時(shí)也存在兩個(gè)問題钻注,即內(nèi)存占用問題和數(shù)據(jù)一致性問題蚂且。所以在開發(fā)的時(shí)候需要注意一下。

內(nèi)存占用問題幅恋。因?yàn)?code>CopyOnWrite的寫時(shí)復(fù)制機(jī)制杏死,所以在進(jìn)行寫操作的時(shí)候,內(nèi)存里會(huì)同時(shí)駐扎兩個(gè)對象的內(nèi)存捆交,舊的對象和新寫入的對象(注意:在復(fù)制的時(shí)候只是復(fù)制容器里的引用淑翼,只是在寫的時(shí)候會(huì)創(chuàng)建新對象添加到新容器里,而舊容器的對象還在使用品追,所以有兩份對象內(nèi)存)玄括。如果這些對象占用的內(nèi)存比較大,比如說200M左右肉瓦,那么再寫入100M數(shù)據(jù)進(jìn)去遭京,內(nèi)存就會(huì)占用300M,那么這個(gè)時(shí)候很有可能造成頻繁的Yong GC和Full GC泞莉。之前我們系統(tǒng)中使用了一個(gè)服務(wù)由于每晚使用CopyOnWrite機(jī)制更新大對象哪雕,造成了每晚15秒的Full GC,應(yīng)用響應(yīng)時(shí)間也隨之變長鲫趁。

針對內(nèi)存占用問題斯嚎,可以通過壓縮容器中的元素的方法來減少大對象的內(nèi)存消耗,比如挨厚,如果元素全是10進(jìn)制的數(shù)字堡僻,可以考慮把它壓縮成36進(jìn)制或64進(jìn)制∫咛辏或者不使用CopyOnWrite容器苦始,而使用其他的并發(fā)容器,如ConcurrentHashMap

數(shù)據(jù)一致性問題慌申。CopyOnWrite容器只能保證數(shù)據(jù)的最終一致性,不能保證數(shù)據(jù)的實(shí)時(shí)一致性。所以如果你希望寫入的的數(shù)據(jù)蹄溉,馬上能讀到咨油,請不要使用CopyOnWrite容器。


六. 對比Collections.synchronizedList

CopyOnWriteArrayListCollections.synchronizedList是實(shí)現(xiàn)線程安全的列表的兩種方式柒爵。兩種實(shí)現(xiàn)方式分別針對不同情況有不同的性能表現(xiàn)役电。

因?yàn)?code>CopyOnWriteArrayList的寫操作不僅有lock鎖,還在內(nèi)部進(jìn)行了數(shù)組的copy棉胀,所以性能比Collections.synchronizedList要低法瑟。

而讀操作CopyOnWriteArrayList直接取的數(shù)組的值,Collections.synchronizedList卻有synchronized修飾唁奢,所以讀性能CopyOnWriteArrayList略勝一籌霎挟。

因此在不同的應(yīng)用場景下,應(yīng)該選擇不同的多線程安全實(shí)現(xiàn)類麻掸。


七. COW vs 讀寫鎖

相同點(diǎn):1. 兩者都是通過讀寫分離的思想實(shí)現(xiàn)酥夭;2.讀線程間是互不阻塞的

不同點(diǎn):對讀線程而言,為了實(shí)現(xiàn)數(shù)據(jù)實(shí)時(shí)性脊奋,在寫鎖被獲取后熬北,讀線程會(huì)等待或者當(dāng)讀鎖被獲取后,寫線程會(huì)等待诚隙,從而解決“臟讀”等問題讶隐。也就是說如果使用讀寫鎖依然會(huì)出現(xiàn)讀線程阻塞等待的情況。而COW則完全放開了犧牲數(shù)據(jù)實(shí)時(shí)性而保證數(shù)據(jù)最終一致性久又,即讀線程對數(shù)據(jù)的更新是延時(shí)感知的巫延,因此讀線程不會(huì)存在等待的情況。


八. CopyOnWriteArrayList透露的思想

?如上面的分析CopyOnWriteArrayList表達(dá)的一些思想:

    1. 讀寫分離籽孙,讀和寫分開
    1. 最終一致性
    1. 使用另外開辟空間的思路烈评,來解決并發(fā)沖突

參考文獻(xiàn)


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市犯建,隨后出現(xiàn)的幾起案子讲冠,更是在濱河造成了極大的恐慌,老刑警劉巖适瓦,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件竿开,死亡現(xiàn)場離奇詭異,居然都是意外死亡玻熙,警方通過查閱死者的電腦和手機(jī)否彩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來嗦随,“玉大人列荔,你說我怎么就攤上這事敬尺。” “怎么了贴浙?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵砂吞,是天一觀的道長。 經(jīng)常有香客問我崎溃,道長蜻直,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任袁串,我火速辦了婚禮概而,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘囱修。我一直安慰自己赎瑰,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布蔚袍。 她就那樣靜靜地躺著乡范,像睡著了一般。 火紅的嫁衣襯著肌膚如雪啤咽。 梳的紋絲不亂的頭發(fā)上晋辆,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天,我揣著相機(jī)與錄音宇整,去河邊找鬼瓶佳。 笑死,一個(gè)胖子當(dāng)著我的面吹牛鳞青,可吹牛的內(nèi)容都是我干的霸饲。 我是一名探鬼主播,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼臂拓,長吁一口氣:“原來是場噩夢啊……” “哼厚脉!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起胶惰,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤傻工,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后孵滞,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體中捆,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年坊饶,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了泄伪。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片翩腐。...
    茶點(diǎn)故事閱讀 38,064評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡扮碧,死狀恐怖空镜,靈堂內(nèi)的尸體忽然破棺而出桑腮,到底是詐尸還是另有隱情,我是刑警寧澤脓杉,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布糟秘,位于F島的核電站,受9級特大地震影響球散,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜散庶,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一蕉堰、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧悲龟,春花似錦屋讶、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至轻腺,卻和暖如春乐疆,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背贬养。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工挤土, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人误算。 一個(gè)月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓仰美,卻偏偏與公主長得像,于是被迫代替她去往敵國和親儿礼。 傳聞我的和親對象是個(gè)殘疾皇子咖杂,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評論 2 345

推薦閱讀更多精彩內(nèi)容