看了CopyOnWriteArrayList后自己實(shí)現(xiàn)了一個(gè)CopyOnWriteHashMap
Python實(shí)戰(zhàn)社群
Java實(shí)戰(zhàn)社群
長(zhǎng)按識(shí)別下方二維碼,按需求添加
掃碼關(guān)注添加客服
進(jìn)Python社群▲
掃碼關(guān)注添加客服
進(jìn)Java社群▲
作者丨碼農(nóng)二胖
來(lái)源丨java金融
引言
面試官: 小伙子你有點(diǎn)眼熟啊,是不是去年來(lái)這面試過(guò)啊。
二胖: 啊,沒(méi)有啊我這是第一次來(lái)這。
面試官: 行,那我們開(kāi)始今天的面試吧,剛開(kāi)始我們先來(lái)點(diǎn)簡(jiǎn)單的吧,java里面的容器你知道哪些啊,跟我說(shuō)一說(shuō)吧。
二胖: 好的,java里面常見(jiàn)容器有ArrayList(線程非安全)、HashMap(線程非安全)、HashSet(線程非安全),ConcurrentHashMap(線程安全)。
面試官: ArrayList 既然線程非安全那有沒(méi)有線程安全的ArrayList列?
二胖: ?這個(gè)。。。好像問(wèn)到知識(shí)盲點(diǎn)了。
面試官: ?那我們今天的面試就先到這了,我待會(huì)還有一個(gè)會(huì),后續(xù)如有通知人事會(huì)聯(lián)系你的。
以上故事純屬虛構(gòu)如有雷同請(qǐng)以本文為主。
什么是COW
在java里面說(shuō)到集合容器我們一般首先會(huì)想到的是HashMap、ArrayList、HasHSet這幾個(gè)容器也是平時(shí)開(kāi)發(fā)中用的最多的。這幾個(gè)都是非線程安全的,如果我們有特定業(yè)務(wù)需要使用線程的安全容器列,
HashMap可以用ConcurrentHashMap代替。ArrayList可以使用Collections.synchronizedList()方法(list每個(gè)方法都用synchronized修飾) 或者使用Vector(現(xiàn)在基本也不用了,每個(gè)方法都用synchronized修飾) 或者使用CopyOnWriteArrayList替代。HasHSet 可以使用 Collections.synchronizedSet或者使用CopyOnWriteArraySet來(lái)代替。(CopyOnWriteArraySet為什么不叫CopyOnWriteHashSet因?yàn)?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(60, 112, 198);">CopyOnWriteArraySet底層是采用CopyOnWriteArrayList來(lái)實(shí)現(xiàn)的) 我們可以看到CopyOnWriteArrayList在線程安全的容器里面多次出現(xiàn)。首先我們來(lái)看看什么是CopyOnWrite?Copy-On-Write簡(jiǎn)稱COW,是一種用于程序設(shè)計(jì)中的優(yōu)化策略。
CopyOnWrite容器即寫(xiě)時(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容器也是一種讀寫(xiě)分離的思想,讀和寫(xiě)不同的容器。
為什么要引入COW
防止ConcurrentModificationException異常
在java里面我們?nèi)绻捎貌徽_的循環(huán)姿勢(shì)去遍歷List時(shí)候,如果一邊遍歷一邊修改拋出java.util.ConcurrentModificationException錯(cuò)誤的。
????????List?list?=?new?ArrayList<>();
????????list.add("張三");
????????list.add("java金融");
????????list.add("javajr.cn");
????????Iterator?iterator?=?list.iterator();
????????while(iterator.hasNext()){
????????????String?content?=?iterator.next();
????????????if("張三".equals(content))?{
????????????????list.remove(content);
????????????}
????????}
上面這個(gè)栗子是會(huì)發(fā)生java.util.ConcurrentModificationException異常的,如果把ArrayList改為CopyOnWriteArrayList 是不會(huì)發(fā)生生異常的。
線程安全的容器
我們?cè)倏聪旅嬉粋€(gè)栗子一個(gè)線程往List里面添加數(shù)據(jù),一個(gè)線程循環(huán)list讀數(shù)據(jù)。
????List?list?=?new?ArrayList<>();
????????list.add("張三");
????????list.add("java金融");
????????list.add("javajr.cn");
????????Thread?t?=?new?Thread(new?Runnable()?{
????????????int?count?=?0;
????????????@Override
????????????public?void?run()?{
????????????????while?(true)?{
????????????????????list.add(count++?+?"");
????????????????}
????????????}
????????});
????????t.start();
????????Thread.sleep(10000);
????????for?(String?s?:?list)?{
????????????System.out.println(s);
????????}
我們運(yùn)行上述代碼也會(huì)發(fā)生ConcurrentModificationException異常,如果把ArrayList換成了CopyOnWriteArrayList就一切正常。
CopyOnWriteArrayList的實(shí)現(xiàn)
通過(guò)上面兩個(gè)栗子我們可以發(fā)現(xiàn)CopyOnWriteArrayList是線程安全的,下面我們就來(lái)一起看看CopyOnWriteArrayList是如何實(shí)現(xiàn)線程安全的。
public?class?CopyOnWriteArrayList<E>
????implements?List<E>,?RandomAccess,?Cloneable,?java.io.Serializable?{
????private?static?final?long?serialVersionUID?=?8673264195747942595L;
????/**?The?lock?protecting?all?mutators?*/
????final?transient?ReentrantLock?lock?=?new?ReentrantLock();
????/**?The?array,?accessed?only?via?getArray/setArray.?*/
????private?transient?volatile?Object[]?array;
從源碼中我們可以知道CopyOnWriteArrayList這和ArrayList底層實(shí)現(xiàn)都是通過(guò)一個(gè)Object的數(shù)組來(lái)實(shí)現(xiàn)的,只不過(guò) CopyOnWriteArrayList的數(shù)組是通過(guò)volatile來(lái)修飾的。
add方法:
????public?boolean?add(E?e)?{
????????//?先獲取鎖
????????final?ReentrantLock?lock?=?this.lock;
????????lock.lock();
????????try?{
????????????Object[]?elements?=?getArray();
????????????int?len?=?elements.length;
????????????//?復(fù)制一個(gè)新的數(shù)組
????????????Object[]?newElements?=?Arrays.copyOf(elements,?len?+?1);
????????????newElements[len]?=?e;
????????????//?把新數(shù)組的值?賦給原數(shù)組
????????????setArray(newElements);
????????????return?true;
????????}?finally?{
????????????//?釋放鎖
????????????lock.unlock();
????????}
????}
上述源碼我們可以發(fā)現(xiàn)比較簡(jiǎn)單,有幾個(gè)點(diǎn)需要稍微注意下
增加數(shù)據(jù)的時(shí)候是通過(guò) ReentrantLock加鎖操作來(lái)(在jdk11的時(shí)候采用了synchronized來(lái)替換ReentrantLock)保證多線程寫(xiě)的時(shí)候只有一個(gè)線程進(jìn)行數(shù)組的復(fù)制,否則的話內(nèi)存中會(huì)有多份被復(fù)制的數(shù)據(jù),導(dǎo)致數(shù)據(jù)錯(cuò)亂。數(shù)組是通過(guò) volatile修飾的,根據(jù)volatile的happens-before規(guī)則,寫(xiě)線程對(duì)數(shù)組引用的修改是可以立即對(duì)讀線程是可見(jiàn)的。通過(guò)寫(xiě)時(shí)復(fù)制來(lái)保證讀寫(xiě)在兩個(gè)不同的數(shù)據(jù)容器中進(jìn)行操作。
自己實(shí)現(xiàn)一個(gè)COW容器
Java并發(fā)包里提供了兩個(gè)使用CopyOnWrite機(jī)制實(shí)現(xiàn)的并發(fā)容器,它們是CopyOnWriteArrayList和CopyOnWriteArraySet,但是并沒(méi)有CopyOnWriteHashMap我們可以按照他的思路自己來(lái)實(shí)現(xiàn)一個(gè)CopyOnWriteHashMap
public?class?CopyOnWriteHashMap<K,?V>?implements?Map<K,?V>,?Cloneable?{
????final?transient?ReentrantLock?lock?=?new?ReentrantLock();
????private?volatile?Map?map;
????public?CopyOnWriteHashMap()?{
????????map?=?new?HashMap<>();
????}
????@Override
????public?V?put(K?key,?V?value)?{
????????final?ReentrantLock?lock?=?this.lock;
????????lock.lock();
????????try?{
????????????Map?newMap?=?new?HashMap(map);
????????????V?val?=?newMap.put(key,?value);
????????????map?=?newMap;
????????????return?val;
????????}?finally?{
????????????lock.unlock();
????????}
????}
????@Override
????public?V?get(Object?key)?{
????????return?map.get(key);
????}
????@Override
????public?V?remove(Object?key)?{
????????final?ReentrantLock?lock?=?this.lock;
????????lock.lock();
????????try?{
????????????Map?newMap?=?new?HashMap(map);
????????????if?(!newMap.containsKey(key))?{
????????????????return?null;
????????????}
????????????V?v?=?newMap.get(key);
????????????newMap.remove(key);
????????????map?=?newMap;
????????????return?v;
????????}finally?{
????????????lock.unlock();
????????}
????}
上述我們實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的CopyOnWriteHashMap,只實(shí)現(xiàn)了add、remove、get方法其他剩余的方法可以自行去實(shí)現(xiàn),涉及到只要數(shù)據(jù)變化的就要加鎖,讀無(wú)需加鎖。
應(yīng)用場(chǎng)景
CopyOnWrite并發(fā)容器適用于讀多寫(xiě)少的并發(fā)場(chǎng)景,比如黑白名單、國(guó)家城市等基礎(chǔ)數(shù)據(jù)緩存、系統(tǒng)配置等。這些基本都是只要想項(xiàng)目啟動(dòng)的時(shí)候初始化一次,變更頻率非常的低。如果這種讀多寫(xiě)少的場(chǎng)景采用 Vector,Collections包裝的這些方式是不合理的,因?yàn)楸M管多個(gè)讀線程從同一個(gè)數(shù)據(jù)容器中讀取數(shù)據(jù),但是讀線程對(duì)數(shù)據(jù)容器的數(shù)據(jù)并不會(huì)發(fā)生發(fā)生修改,所以并不需要讀也加鎖。
CopyOnWrite缺點(diǎn)
CopyOnWriteArrayList雖然是一個(gè)線程安全版的ArrayList,但其每次修改數(shù)據(jù)時(shí)都會(huì)復(fù)制一份數(shù)據(jù)出來(lái),所以CopyOnWriteArrayList只適用讀多寫(xiě)少或無(wú)鎖讀場(chǎng)景。我們?nèi)绻趯?shí)際業(yè)務(wù)中使用CopyOnWriteArrayList,一定是因?yàn)檫@個(gè)場(chǎng)景適合而非是為了炫技。
內(nèi)存占用問(wèn)題
因?yàn)镃opyOnWrite的寫(xiě)時(shí)復(fù)制機(jī)制每次進(jìn)行寫(xiě)操作的時(shí)候都會(huì)有兩個(gè)數(shù)組對(duì)象的內(nèi)存,如果這個(gè)數(shù)組對(duì)象占用的內(nèi)存較大的話,如果頻繁的進(jìn)行寫(xiě)入就會(huì)造成頻繁的Yong GC和Full GC。
數(shù)據(jù)一致性問(wèn)題
CopyOnWrite容器只能保證數(shù)據(jù)的最終一致性,不能保證數(shù)據(jù)的實(shí)時(shí)一致性。讀操作的線程可能不會(huì)立即讀取到新修改的數(shù)據(jù),因?yàn)樾薷牟僮靼l(fā)生在副本上。但最終修改操作會(huì)完成并更新容器所以這是最終一致性。
CopyOnWriteArrayList和Collections.synchronizedList()
簡(jiǎn)單的測(cè)試了下CopyOnWriteArrayList ?和 ?Collections.synchronizedList()的讀和寫(xiě)發(fā)現(xiàn):
在高并發(fā)的寫(xiě)時(shí)CopyOnWriteArray比同步Collections.synchronizedList慢百倍 在高并發(fā)的讀性能時(shí)CopyOnWriteArray比同步Collections.synchronizedList快幾十倍。 高并發(fā)寫(xiě)時(shí),CopyOnWriteArrayList為何這么慢呢?因?yàn)槠涿看蝍dd時(shí),都用Arrays.copyOf創(chuàng)建新數(shù)組,頻繁add時(shí)內(nèi)存申請(qǐng)釋放性能消耗大。 高并發(fā)讀的時(shí)候CopyOnWriteArray無(wú)鎖,Collections.synchronizedList有鎖所以讀的效率比較低下。
總結(jié)
選擇CopyOnWriteArrayList的時(shí)候一定是讀遠(yuǎn)大于寫(xiě)。如果讀寫(xiě)都差不多的話建議選擇Collections.synchronizedList。
巨人肩膀摘蘋(píng)果
http://ifeve.com/java-copy-on-write/


近期精彩內(nèi)容推薦:??
?中國(guó)男性的私密數(shù)據(jù)大賞,女生勿入!
?一個(gè)月薪 12000 的北京程序員的真實(shí)生活 !

