詳解ConcurrentHashMap及JDK8的優(yōu)化

由于HashMap在并發(fā)中會出現(xiàn)一些問題,所以JDK中提供了并發(fā)容器ConcurrentHashMap。有關(guān)HashMap并發(fā)中的問題和原理,強(qiáng)烈建議查看這篇文章進(jìn)行復(fù)習(xí)

ConcurrentHashMap使用分段鎖技術(shù)吧史,將整個數(shù)據(jù)結(jié)構(gòu)分段(默認(rèn)為16段)進(jìn)行存儲,然后給每一段數(shù)據(jù)配一把鎖(繼承ReentrantLock)唠雕,當(dāng)一個線程占用鎖訪問其中一個段的數(shù)據(jù)的時候贸营,其他段的數(shù)據(jù)仍然能被其他線程訪問,能夠?qū)崿F(xiàn)真正的并發(fā)訪問岩睁。下圖為JDK7的數(shù)據(jù)結(jié)構(gòu)钞脂。

ConcurrentHashMap使用分段鎖技術(shù)

JDK7的操作

JDK7的put過程

  1. 首先對key進(jìn)行第1次hash,通過hash值確定segment的位置
  2. 然后在segment內(nèi)進(jìn)行操作捕儒,獲取鎖
  3. 獲取當(dāng)前segment的HashEntry數(shù)組后對key進(jìn)行第2次hash冰啃,通過hash值確定在HashEntry數(shù)組的索引位置
  4. 通過繼承ReentrantLock的tryLock方法嘗試去獲取鎖,如果獲取成功就直接插入相應(yīng)的位置刘莹,如果已經(jīng)有線程獲取該Segment的鎖阎毅,那當(dāng)前線程會以自旋的方式去繼續(xù)的調(diào)用tryLock方法去獲取鎖,超過指定次數(shù)就掛起点弯,等待喚醒
  5. 然后對當(dāng)前索引的HashEntry鏈進(jìn)行遍歷扇调,如果有重復(fù)的key,則替換抢肛;如果沒有重復(fù)的狼钮,則插入到鏈頭
  6. 釋放鎖

get操作和put操作類似,也是要兩次hash捡絮。但是get操作的concurrenthashmap不需要加鎖燃领,原因是將存儲元素都標(biāo)記了volatile。要是不明白volatile锦援,歡迎復(fù)習(xí)這篇博客

JDK7的size過程

  1. size操作就是遍歷了兩次所有的Segments,每次記錄Segment的modCount值剥悟,然后將兩次的modCount進(jìn)行比較灵寺,如果相同曼库,則表示期間沒有發(fā)生過寫入操作,就將原先遍歷的結(jié)果返回略板。
  2. 如果經(jīng)判斷發(fā)現(xiàn)兩次統(tǒng)計(jì)出的modCount并不一致毁枯,要重新啟用全部segment加鎖的方式來進(jìn)行count的獲取和統(tǒng)計(jì)了,這樣在此期間每個segement都被鎖住叮称,無法進(jìn)行其他操作种玛,統(tǒng)計(jì)出的count自然很準(zhǔn)確。

在寫操作put瓤檐,remove赂韵,擴(kuò)容的時候,會對Segment加鎖挠蛉,只影響當(dāng)前Segment祭示,其他的Segment還是可以并發(fā)的

JDK8的優(yōu)化總結(jié)

JDK8的ConcurrentHashMap的數(shù)據(jù)結(jié)構(gòu)已經(jīng)接近對應(yīng)版本的HashMap,了解Hashmap的結(jié)構(gòu)谴古,就基本了解了Concurrenthashmap了质涛,只是增加了同步的操作來控制并發(fā)。從JDK7版本的ReentrantLock+Segment+HashEntry掰担,到JDK8版本中synchronized+CAS+HashEntry+紅黑樹汇陆。

JDK8的ConcurrentHashMap
  1. 數(shù)據(jù)結(jié)構(gòu):取消了Segment分段鎖的數(shù)據(jù)結(jié)構(gòu),取而代之的是Node數(shù)組+鏈表+紅黑樹的結(jié)構(gòu)带饱,從而實(shí)現(xiàn)了對每一行數(shù)據(jù)進(jìn)行加鎖毡代,進(jìn)一步減少并發(fā)沖突的概率。

Node類成員變量Node的元素val和指針next都標(biāo)注volatile纠炮,目的是在多線程環(huán)境下線程A修改結(jié)點(diǎn)的val或者新增節(jié)點(diǎn)的時候是對線程B可見的月趟。

ConcurrentHashMap有成員變量transient volatile Node<K,V>[] table,目的是為了使Node數(shù)組在擴(kuò)容的時候?qū)ζ渌€程具有可見性而加的volatile恢口。(例如:volatile int array[10]是指array的地址是volatile的而不是數(shù)組元素的值是volatile的.)

  1. 保證線程安全機(jī)制:JDK7采用segment的分段鎖機(jī)制實(shí)現(xiàn)線程安全孝宗,其中segment繼承自ReentrantLock。JDK8采用CAS(讀)+Synchronized(寫)保證線程安全耕肩。
  2. 鎖的粒度:原來是對需要進(jìn)行數(shù)據(jù)操作的Segment加鎖因妇,JDK8調(diào)整為對每個數(shù)組元素加鎖(Node)。
  3. 鏈表轉(zhuǎn)化為紅黑樹:定位結(jié)點(diǎn)的hash算法簡化會帶來弊端猿诸,Hash沖突加劇婚被,因此在鏈表節(jié)點(diǎn)數(shù)量大于8時,會將鏈表轉(zhuǎn)化為紅黑樹進(jìn)行存儲梳虽。
  4. 查詢時間復(fù)雜度:從原來的遍歷鏈表O(n)址芯,變成遍歷紅黑樹O(logN)。
  5. JDK8推薦使用mappingCount方法而不是size方法獲取當(dāng)前map表的大小,因?yàn)檫@個方法的返回值是long類型谷炸,size方法是返回值類型是int北专。

JDK8的get過程

  1. 計(jì)算hash值,定位到該table索引位置旬陡,如果是首節(jié)點(diǎn)符合就返回
  2. 如果遇到擴(kuò)容的時候拓颓,會調(diào)用標(biāo)志正在擴(kuò)容節(jié)點(diǎn)ForwardingNode的find方法,查找該節(jié)點(diǎn)描孟,匹配就返回
  3. 以上都不符合的話驶睦,就往下遍歷節(jié)點(diǎn),匹配就返回匿醒,否則最后就返回null

寫到這的時候场航,筆者建議大家去了解下Redis的漸進(jìn)式擴(kuò)容,是另一種思想青抛,都值得學(xué)習(xí)旗闽。一句話幫助理解Redis的漸進(jìn)式擴(kuò)容:由于Redis是單線程,而且數(shù)據(jù)量較大時蜜另,無法一次性快速擴(kuò)容适室,所以Redis首先申請一個新的容量加倍的哈希表,然后在插入举瑰,刪除捣辆,更新操作的時候,調(diào)用rehash函數(shù)(dictRehash函數(shù))此迅,將原有操作單元的鏈表移植到新的哈希表中汽畴,當(dāng)原有哈希表全部移植過去,擴(kuò)容結(jié)束耸序。

JDK8的size過程

兩個重要變量:

  1. baseCount用于記錄節(jié)點(diǎn)的個數(shù)忍些,是個volatile變量
  2. counterCells是一個輔助baseCount計(jì)數(shù)的數(shù)組,每個counterCell存著部分的節(jié)點(diǎn)數(shù)量坎怪,這樣做的目的就是盡可能地減少沖突罢坝。

counterCell類使用了 @sun.misc.Contended 標(biāo)記的類,內(nèi)部一個 volatile變量搅窿。這個注解標(biāo)識著這個類需要避免 "偽共享".

避免偽共享(false sharing)嘁酿。緩存系統(tǒng)中是以緩存行(cache line)為單位存儲的。緩存行是2的整數(shù)冪個連續(xù)字節(jié)男应,
一般為32-256個字節(jié)闹司。最常見的緩存行大小是64個字節(jié)。當(dāng)多線程修改互相獨(dú)立的變量時沐飘,如果這些變量共享同一個緩存行游桩,就會無意中影響彼此的性能牲迫,這就是偽共享。所以偽共享對性能危害很大众弓。
沒有這個注解之前恩溅,是通過使用拼接把緩存行加滿來解決這個問題,讓緩存之間的修改互不影響谓娃。

ConcurrentHashMap節(jié)點(diǎn)的數(shù)量 = baseCount+counterCells每個cell記錄下來的節(jié)點(diǎn)數(shù)量

由于JDK8在統(tǒng)計(jì)這個數(shù)量的時候并沒有進(jìn)行加鎖,所以這個結(jié)果并不是絕對準(zhǔn)確的蜒滩。原理都是相通的滨达,可以順道看看LongAdder的longValue方法。想學(xué)習(xí)更多J.U.C的原子操作類俯艰,可以查看這篇博客

更新size的過程:

總體的原則就是:先嘗試更新baseCount捡遍,失敗再利用CounterCell。

  1. 通過CAS嘗試更新baseCount 竹握,如果更新成功則完成画株,如果CAS更新失敗會進(jìn)入下一步
  2. 線程通過隨機(jī)數(shù)ThreadLocalRandom.getProbe() & (n-1) 計(jì)算出在counterCells數(shù)組的位置,如果不為null啦辐,則CAS嘗試在couterCell上直接增加數(shù)量谓传,如果失敗,counterCells數(shù)組會進(jìn)行擴(kuò)容為原來的兩倍芹关,繼續(xù)隨機(jī)续挟,繼續(xù)添加

JDK8的put過程

對當(dāng)前的table進(jìn)行無條件自循環(huán)直到put成功

  1. 如果沒有初始化就先調(diào)用initTable()方法來進(jìn)行初始化過程
  2. 如果沒有hash沖突就直接CAS插入
  3. 如果還在進(jìn)行擴(kuò)容操作就先進(jìn)行擴(kuò)容
  4. 如果存在hash沖突,就加synchronized鎖來保證線程安全侥衬,這里有兩種情況诗祸,一種是鏈表形式就直接遍歷到尾端插入,一種是紅黑樹就按照紅黑樹結(jié)構(gòu)插入轴总,
  5. 最后一個如果該鏈表的數(shù)量大于閾值8直颅,就要先轉(zhuǎn)換成黑紅樹的結(jié)構(gòu),break再一次進(jìn)入循環(huán)
  6. 如果添加成功就調(diào)用addCount方法統(tǒng)計(jì)size怀樟,并且檢查是否需要擴(kuò)容

FAQ

ConcurrentHashMap迭代器是強(qiáng)一致性還是弱一致性功偿?

ConcurrentHashMap迭代器是弱一致性,hashmap迭代器是強(qiáng)一直性漂佩。

ConcurrentHashMap可以支持在迭代過程中脖含,向map添加新元素,而HashMap則拋出了ConcurrentModificationException投蝉,因?yàn)镠ashMap包含一個修改計(jì)數(shù)器养葵,當(dāng)你調(diào)用他的next()方法來獲取下一個元素時,迭代器將會用到這個計(jì)數(shù)器瘩缆。

ConcurrentHashMap一定線程安全么关拒?

ConcurrentHashMap使用不當(dāng),也會造成死循環(huán)的。以后會有博客來介紹~

哎呀着绊,如果我的名片丟了谐算。微信搜索“全菜工程師小輝”,依然可以找到我
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末归露,一起剝皮案震驚了整個濱河市洲脂,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌剧包,老刑警劉巖恐锦,帶你破解...
    沈念sama閱讀 212,542評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異疆液,居然都是意外死亡一铅,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,596評論 3 385
  • 文/潘曉璐 我一進(jìn)店門堕油,熙熙樓的掌柜王于貴愁眉苦臉地迎上來潘飘,“玉大人,你說我怎么就攤上這事掉缺〔仿迹” “怎么了?”我有些...
    開封第一講書人閱讀 158,021評論 0 348
  • 文/不壞的土叔 我叫張陵攀圈,是天一觀的道長暴凑。 經(jīng)常有香客問我,道長赘来,這世上最難降的妖魔是什么现喳? 我笑而不...
    開封第一講書人閱讀 56,682評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮犬辰,結(jié)果婚禮上嗦篱,老公的妹妹穿的比我還像新娘。我一直安慰自己幌缝,他們只是感情好灸促,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,792評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著涵卵,像睡著了一般浴栽。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上轿偎,一...
    開封第一講書人閱讀 49,985評論 1 291
  • 那天典鸡,我揣著相機(jī)與錄音,去河邊找鬼坏晦。 笑死萝玷,一個胖子當(dāng)著我的面吹牛嫁乘,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播球碉,決...
    沈念sama閱讀 39,107評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼蜓斧,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了睁冬?” 一聲冷哼從身側(cè)響起挎春,我...
    開封第一講書人閱讀 37,845評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎豆拨,沒想到半個月后搂蜓,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,299評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡辽装,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,612評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了相味。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拾积。...
    茶點(diǎn)故事閱讀 38,747評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖丰涉,靈堂內(nèi)的尸體忽然破棺而出拓巧,到底是詐尸還是另有隱情,我是刑警寧澤一死,帶...
    沈念sama閱讀 34,441評論 4 333
  • 正文 年R本政府宣布肛度,位于F島的核電站,受9級特大地震影響投慈,放射性物質(zhì)發(fā)生泄漏承耿。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,072評論 3 317
  • 文/蒙蒙 一伪煤、第九天 我趴在偏房一處隱蔽的房頂上張望加袋。 院中可真熱鬧,春花似錦抱既、人聲如沸职烧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,828評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蚀之。三九已至,卻和暖如春捷泞,著一層夾襖步出監(jiān)牢的瞬間足删,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,069評論 1 267
  • 我被黑心中介騙來泰國打工肚邢, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留壹堰,地道東北人拭卿。 一個月前我還...
    沈念sama閱讀 46,545評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像贱纠,于是被迫代替她去往敵國和親峻厚。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,658評論 2 350

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