【細(xì)談Java并發(fā)】談?wù)凜opy-On-Write容器

1忿危、簡(jiǎn)介

Copy-On-Write簡(jiǎn)稱COW,是一種用于程序設(shè)計(jì)中的優(yōu)化策略七扰。其基本思路是奢赂,從一開(kāi)始大家都在共享同一個(gè)內(nèi)容,當(dāng)某個(gè)人想要修改這個(gè)內(nèi)容的時(shí)候颈走,才會(huì)真正把內(nèi)容Copy出去形成一個(gè)新的內(nèi)容然后再改膳灶,這是一種延時(shí)懶惰策略。從JDK1.5開(kāi)始Java并發(fā)包里提供了兩個(gè)使用CopyOnWrite機(jī)制實(shí)現(xiàn)的并發(fā)容器,它們是CopyOnWriteArrayList和CopyOnWriteArraySet立由。CopyOnWrite容器非常有用轧钓,可以在非常多的并發(fā)場(chǎng)景中使用到司致。

CopyOnWrite容器即寫時(shí)復(fù)制的容器。通俗的理解是當(dāng)我們往一個(gè)容器添加元素的時(shí)候聋迎,不直接往當(dāng)前容器添加脂矫,而是先將當(dāng)前容器進(jìn)行Copy,復(fù)制出一個(gè)新的容器霉晕,然后新的容器里添加元素庭再,添加完元素之后,再將原容器的引用指向新的容器牺堰。這樣做的好處是我們可以對(duì)CopyOnWrite容器進(jìn)行并發(fā)的讀拄轻,而不需要加鎖,因?yàn)楫?dāng)前容器不會(huì)添加任何元素伟葫。所以CopyOnWrite容器也是一種讀寫分離的思想恨搓,讀和寫不同的容器。

2筏养、CopyOnWriteArrayList的實(shí)現(xiàn)原理

在使用CopyOnWriteArrayList之前斧抱,我們先閱讀其源碼了解下它是如何實(shí)現(xiàn)的。以下代碼是向ArrayList里添加元素渐溶,可以發(fā)現(xiàn)在添加的時(shí)候是需要加鎖的辉浦,否則多線程寫的時(shí)候會(huì)Copy出N個(gè)副本出來(lái)。

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        // 復(fù)制出新數(shù)組
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 把新元素添加到新數(shù)組里
        newElements[len] = e;
        // 把原數(shù)組引用指向新數(shù)組
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

讀的時(shí)候不需要加鎖茎辐,如果讀的時(shí)候有多個(gè)線程正在向ArrayList添加數(shù)據(jù)宪郊,讀還是會(huì)讀到舊的數(shù)據(jù),因?yàn)閷懙臅r(shí)候不會(huì)鎖住舊的ArrayList拖陆,因?yàn)閿?shù)組對(duì)象是用volatile修飾的弛槐,所以多線程之間,數(shù)組會(huì)進(jìn)行共享依啰,當(dāng)有一個(gè)線程修改了其值乎串,會(huì)立即同步到主存中,當(dāng)下一次再get的時(shí)候會(huì)發(fā)現(xiàn)和上一次get的值不同孔飒,因此讀操作會(huì)發(fā)生臟讀灌闺。

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

public E get(int index) {
    return get(getArray(), index);
}

我們?cè)賮?lái)看看它的迭代器

static final class COWIterator<E> implements ListIterator<E> {
    private final Object[] snapshot;
    private int cursor;

    private COWIterator(Object[] elements, int initialCursor) {
        cursor = initialCursor;
        snapshot = elements;
    }

    public boolean hasNext() { return cursor < snapshot.length; }
    public boolean hasPrevious() { return cursor > 0; }

    @SuppressWarnings("unchecked")
    public E next() {
        if (! hasNext())
            throw new NoSuchElementException();
        return (E) snapshot[cursor++];
    }

    @SuppressWarnings("unchecked")
    public E previous() {
        if (! hasPrevious())
            throw new NoSuchElementException();
        return (E) snapshot[--cursor];
    }

    public int nextIndex() { return cursor; }
    public int previousIndex() { return cursor-1; }

    public void remove() { throw new UnsupportedOperationException(); }
    public void set(E e) { throw new UnsupportedOperationException(); }
    public void add(E e) { throw new UnsupportedOperationException(); }
}

我們知道ArrayList是非線性安全的集合艰争,它使用迭代器時(shí)有可能會(huì)拋出ConcurrentModificationException坏瞄。而且并發(fā)的時(shí)候進(jìn)行插入操作時(shí),由于沒(méi)有進(jìn)行同步操作甩卓,容易丟失數(shù)據(jù)鸠匀。

可以看到,在COWIterator的迭代中逾柿,不能直接增刪改缀棍,避免了ConcurrentModificationException宅此。

3、CopyOnWriteArraySet的實(shí)現(xiàn)原理

通過(guò)源碼可以看到爬范,CopyOnWriteArraySet內(nèi)部維護(hù)著一個(gè)CopyOnWriteArrayList父腕,所有的操作都是通過(guò)它去完成的。

private final CopyOnWriteArrayList<E> al;

/**
 * Creates an empty set.
 */
public CopyOnWriteArraySet() {
    al = new CopyOnWriteArrayList<E>();
}

我們主要看看add(E)方法的實(shí)現(xiàn)青瀑,它調(diào)用了CopyOnWriteArrayList的addIfAbsent方法

public boolean addIfAbsent(E e) {
    // 當(dāng)前數(shù)組的一個(gè)快照
    Object[] snapshot = getArray();
    // 如果傳入的元素存在璧亮,直接返回false,否則增加
    return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
        addIfAbsent(e, snapshot);
}

private boolean addIfAbsent(E e, Object[] snapshot) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] current = getArray();
        int len = current.length;
        // 傳入的數(shù)組和當(dāng)前數(shù)組不一樣
        if (snapshot != current) {
            // Optimize for lost race to another addXXX operation
            int common = Math.min(snapshot.length, len);
            for (int i = 0; i < common; i++)
                if (current[i] != snapshot[i] && eq(e, current[i]))
                    return false;
            if (indexOf(e, current, common, len) >= 0)
                    return false;
        }
        // 創(chuàng)建一個(gè)新數(shù)組斥难,長(zhǎng)度為原數(shù)組長(zhǎng)度+1
        Object[] newElements = Arrays.copyOf(current, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

4枝嘶、自己實(shí)現(xiàn)CopyOnWriteMap

JDK中并沒(méi)有提供CopyOnWriteMap,我們可以參考CopyOnWriteArrayList來(lái)實(shí)現(xiàn)一個(gè)哑诊,基本代碼如下:

import java.util.Collection;
import java.util.Map;
import java.util.Set;

public class CopyOnWriteMap<K, V> implements Map<K, V>, Cloneable {
    private volatile Map<K, V> internalMap;
 
    public CopyOnWriteMap() {
        internalMap = new HashMap<K, V>();
    }
 
    public V put(K key, V value) {
 
        synchronized (this) {
            Map<K, V> newMap = new HashMap<K, V>(internalMap);
            V val = newMap.put(key, value);
            internalMap = newMap;
            return val;
        }
    }
 
    public V get(Object key) {
        return internalMap.get(key);
    }
 
    public void putAll(Map<? extends K, ? extends V> newData) {
        synchronized (this) {
            Map<K, V> newMap = new HashMap<K, V>(internalMap);
            newMap.putAll(newData);
            internalMap = newMap;
        }
    }
}

實(shí)現(xiàn)很簡(jiǎn)單群扶,只要了解了CopyOnWrite機(jī)制,我們可以實(shí)現(xiàn)各種CopyOnWrite容器镀裤,并且在不同的應(yīng)用場(chǎng)景中使用竞阐。

5、CopyOnWrite的應(yīng)用場(chǎng)景

CopyOnWrite并發(fā)容器用于讀多寫少的并發(fā)場(chǎng)景暑劝。比如白名單馁菜,黑名單,商品類目的訪問(wèn)和更新場(chǎng)景铃岔,假如我們有一個(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)代碼如下:

package com.github.book;
 
import java.util.Map;
 
import com.github.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);
    }
 
}

代碼很簡(jiǎn)單,但是使用CopyOnWriteMap需要注意兩件事情:

  1. 減少擴(kuò)容開(kāi)銷嫁艇。根據(jù)實(shí)際需要朗伶,初始化CopyOnWriteMap的大小,避免寫時(shí)CopyOnWriteMap擴(kuò)容的開(kāi)銷步咪。
  2. 使用批量添加论皆。因?yàn)槊看翁砑樱萜髅看味紩?huì)進(jìn)行復(fù)制,所以減少添加次數(shù)点晴,可以減少容器的復(fù)制次數(shù)感凤。如使用上面代碼里的addBlackList方法。

6粒督、CopyOnWrite的缺點(diǎn)

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

內(nèi)存占用問(wèn)題。因?yàn)镃opyOnWrite的寫時(shí)復(fù)制機(jī)制仇矾,所以在進(jìn)行寫操作的時(shí)候庸蔼,內(nèi)存里會(huì)同時(shí)駐扎兩個(gè)對(duì)象的內(nèi)存,舊的對(duì)象和新寫入的對(duì)象(注意:在復(fù)制的時(shí)候只是復(fù)制容器里的引用贮匕,只是在寫的時(shí)候會(huì)創(chuàng)建新對(duì)象添加到新容器里姐仅,而舊容器的對(duì)象還在使用,所以有兩份對(duì)象內(nèi)存)刻盐。如果這些對(duì)象占用的內(nèi)存比較大掏膏,比如說(shuō)200M左右,那么再寫入100M數(shù)據(jù)進(jìn)去敦锌,內(nèi)存就會(huì)占用300M馒疹,那么這個(gè)時(shí)候很有可能造成頻繁的Yong GC和Full GC。之前我們系統(tǒng)中使用了一個(gè)服務(wù)由于每晚使用CopyOnWrite機(jī)制更新大對(duì)象乙墙,造成了每晚15秒的Full GC颖变,應(yīng)用響應(yīng)時(shí)間也隨之變長(zhǎng)。

針對(duì)內(nèi)存占用問(wèn)題听想,可以通過(guò)壓縮容器中的元素的方法來(lái)減少大對(duì)象的內(nèi)存消耗腥刹,比如,如果元素全是10進(jìn)制的數(shù)字汉买,可以考慮把它壓縮成36進(jìn)制或64進(jìn)制衔峰。或者不使用CopyOnWrite容器蛙粘,而使用其他的并發(fā)容器垫卤,如ConcurrentHashMap。

數(shù)據(jù)一致性問(wèn)題出牧。CopyOnWrite容器只能保證數(shù)據(jù)的最終一致性穴肘,不能保證數(shù)據(jù)的實(shí)時(shí)一致性。所以如果你希望寫入的的數(shù)據(jù)崔列,馬上能讀到梢褐,請(qǐng)不要使用CopyOnWrite容器旺遮。

7赵讯、對(duì)比Collections.synchronizedList

CopyOnWriteArrayList和Collections.synchronizedList是實(shí)現(xiàn)線程安全的列表的兩種方式盈咳。兩種實(shí)現(xiàn)方式分別針對(duì)不同情況有不同的性能表現(xiàn)。

因?yàn)镃opyOnWriteArrayList的寫操作不僅有l(wèi)ock鎖边翼,還在內(nèi)部進(jìn)行了數(shù)組的copy鱼响,所以性能比Collections.synchronizedList要低。

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

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

總結(jié):CopyOnWriteArrayList,發(fā)生修改時(shí)候做copy厌均,新老版本分離唬滑,保證讀的高性能,適用于以讀為主棺弊,讀操作遠(yuǎn)遠(yuǎn)大于寫操作的場(chǎng)景中使用晶密,比如緩存。而Collections.synchronizedList則可以用在CopyOnWriteArrayList不適用模她,但是有需要同步列表的地方稻艰,讀寫操作都比較均勻的地方。

參考:JAVA中的COPYONWRITE容器

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末侈净,一起剝皮案震驚了整個(gè)濱河市尊勿,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌畜侦,老刑警劉巖运怖,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異夏伊,居然都是意外死亡摇展,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門溺忧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)咏连,“玉大人,你說(shuō)我怎么就攤上這事鲁森∷畹危” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵歌溉,是天一觀的道長(zhǎng)垄懂。 經(jīng)常有香客問(wèn)我骑晶,道長(zhǎng),這世上最難降的妖魔是什么草慧? 我笑而不...
    開(kāi)封第一講書人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任桶蛔,我火速辦了婚禮,結(jié)果婚禮上漫谷,老公的妹妹穿的比我還像新娘仔雷。我一直安慰自己,他們只是感情好舔示,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布碟婆。 她就那樣靜靜地躺著,像睡著了一般惕稻。 火紅的嫁衣襯著肌膚如雪竖共。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 51,115評(píng)論 1 296
  • 那天俺祠,我揣著相機(jī)與錄音公给,去河邊找鬼。 笑死锻煌,一個(gè)胖子當(dāng)著我的面吹牛妓布,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播宋梧,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼匣沼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了捂龄?” 一聲冷哼從身側(cè)響起释涛,我...
    開(kāi)封第一講書人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎倦沧,沒(méi)想到半個(gè)月后唇撬,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡展融,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年窖认,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片告希。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡扑浸,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出燕偶,到底是詐尸還是另有隱情喝噪,我是刑警寧澤,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布指么,位于F島的核電站酝惧,受9級(jí)特大地震影響榴鼎,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜晚唇,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一巫财、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧缺亮,春花似錦翁涤、人聲如沸桥言。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)号阿。三九已至并鸵,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間扔涧,已是汗流浹背园担。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留枯夜,地道東北人弯汰。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像湖雹,于是被迫代替她去往敵國(guó)和親咏闪。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353

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