圖解JDK容器三大將之--哈希表(HashMap)

JDK容器三大將

任何一項(xiàng)新的技術(shù)轰豆、一種新的語(yǔ)言本質(zhì)上都是算法+數(shù)據(jù)結(jié)構(gòu)我注。任何技術(shù)的選型本質(zhì)上都是在基于業(yè)務(wù)和硬件條件的充分理解呀洲,采用合適的數(shù)據(jù)結(jié)構(gòu)审胸、適當(dāng)?shù)乃惴ㄒ赃_(dá)到資源和效率的最優(yōu)解尼变。Java開(kāi)發(fā)亦是如此利凑,工欲善其事,必先利其器嫌术,想要使用Java這種語(yǔ)言開(kāi)發(fā)好程序哀澈,就必須選擇合適的數(shù)據(jù)結(jié)構(gòu)來(lái)進(jìn)行開(kāi)發(fā)。JDK容器則是所有Java應(yīng)用開(kāi)發(fā)的基礎(chǔ)度气,不論是業(yè)務(wù)代碼割按,還是各種知名的Java開(kāi)源項(xiàng)目(如異步網(wǎng)絡(luò)框架netty、容器管理框架Spring磷籍、分布式計(jì)算框架Hadoop适荣、搜索引擎框架Elastic Search),全都是大量在JDK容器的基礎(chǔ)上進(jìn)行開(kāi)發(fā)院领。而JDK容器作為這些優(yōu)秀開(kāi)源項(xiàng)目的默認(rèn)選擇弛矛,必然有其優(yōu)勢(shì),本文將分析下JDK容器三大將之哈希表比然。

JDK容器三大將:List丈氓、Set、Map

JDK容器三大將:List强法、Set万俗、Map

HashMap介紹

所有編程語(yǔ)言都躲不開(kāi)數(shù)據(jù)結(jié)構(gòu)原理中的幾種經(jīng)典結(jié)構(gòu),鏈表饮怯、線性表闰歪、哈希表等。哈希表以其O(1) 的查找耗時(shí)在查找速度方面傲視群雄硕淑,Java中哈希表的實(shí)現(xiàn)即HashMap類课竣。
(原JDK中還有一個(gè)Hashtable類,由于put和get操作都加了synchronized鎖導(dǎo)致單線程性能差置媳,多線程又有基于分段鎖的ConcurrentHashMap作為更好的選擇于樟,官方已經(jīng)聲明不在維護(hù))

先來(lái)看下HashMap的繼承關(guān)系圖如下


HashMap的繼承關(guān)系圖
  • Map接口為實(shí)現(xiàn)類暴露了一系列方法,AbstractMap抽象類為Map的一些基礎(chǔ)功能提供了簡(jiǎn)單實(shí)現(xiàn)
  • Cloneable表示該類支持通過(guò)java.lang.Object#clone()方法克隆拇囊,clone方法相關(guān)的細(xì)節(jié)(淺拷貝迂曲、深拷貝)讀者可以搜索相關(guān)關(guān)鍵字閱讀
  • Serializable接口表示該類支持Java自帶序列化功能進(jìn)行序列化

HashMap的數(shù)據(jù)結(jié)構(gòu)

HashMap的數(shù)據(jù)結(jié)構(gòu)如下圖


HashMap的數(shù)據(jù)結(jié)構(gòu)

下面分別來(lái)介紹下圖中的幾個(gè)概念:

  • size,存著HashMap當(dāng)前的大小寥袭,執(zhí)行hashMap.size()時(shí)直接在O(1)的時(shí)間返回哈希表的大小路捧,如圖有著三個(gè)鍵值對(duì)关霸,所以size=3
  • modCount,記錄哈希表結(jié)構(gòu)變更的次數(shù)杰扫,用于在HashMap結(jié)構(gòu)變更時(shí)能夠快速失敗队寇,后面會(huì)詳細(xì)介紹
  • table,是一個(gè)Node數(shù)組(默認(rèn)大小16)章姓,Node是HashMap的內(nèi)部定義類佳遣,結(jié)構(gòu)如圖,保存著一對(duì)鍵值對(duì)凡伊。當(dāng)執(zhí)行map.put(k, v)命令時(shí)會(huì)根據(jù)k的哈希值映射到Node數(shù)組的某個(gè)位置零渐,并且通過(guò)拉鏈法處理哈希沖突
  • loadFactor、threshold系忙,分別為裝載因子(默認(rèn)0.75)和閾值(Node.length * 裝載因子)诵盼,當(dāng)HashMap的容量(即size)超過(guò)閾值時(shí)會(huì)觸發(fā)rehash,對(duì)Node數(shù)組進(jìn)行擴(kuò)容

HashMap的put/get操作執(zhí)行過(guò)程

put()

HashMap的put操作的執(zhí)行過(guò)程偽代碼如下

void put(key, value) {
    index = hash(key);
    if(table[index] == null)
        table[index] = newNode(key, value); // 節(jié)點(diǎn)沒(méi)有hash沖突時(shí)將該kv錄入節(jié)點(diǎn)
    else
        insertIntoLastNode(table[index], key, value); // 節(jié)點(diǎn)hash沖突時(shí)寫(xiě)入拉鏈的最后
}
  1. 當(dāng)hash沖突寫(xiě)入拉鏈時(shí)银还,HashMap有著一個(gè)常量TREEIFY_THRESHOLD=8风宁,當(dāng)拉鏈長(zhǎng)度超過(guò)閾值后鏈表會(huì)升級(jí)為紅黑樹(shù)
  2. 上文提到過(guò)的參數(shù)threshold = Node.length * 裝載因子,當(dāng)put完的哈希表節(jié)點(diǎn)總數(shù)達(dá)到Node數(shù)組容量的0.75時(shí)會(huì)觸發(fā)擴(kuò)容

get()

HashMap的get操作執(zhí)行過(guò)程的偽代碼如下见剩,都是經(jīng)典的拉鏈法哈希查找的步驟

V get(key) {
    index = hash(key);
    if(table[index].key.hash == key.hash && table[index] == key)
        return table[index].value; // 如上圖中Node[2]定位到的第一個(gè)Node杀糯,如果對(duì)比相等,則返回value
    else
        return findNodeOrNullFromList(key); // 從拉鏈中查找該key的值
}

查找時(shí)同樣也會(huì)需要根據(jù)當(dāng)前是鏈表還是紅黑樹(shù)走不同的查詢邏輯

HashMap的擴(kuò)容過(guò)程

接下來(lái)重點(diǎn)看下HashMap的擴(kuò)容過(guò)程苍苞,上文提到固翰,HashMap在put的時(shí)候,如果put完的哈希表節(jié)點(diǎn)總數(shù)達(dá)到threshold羹呵,則會(huì)進(jìn)行HashMap的擴(kuò)容骂际,擴(kuò)容的操作過(guò)程如下圖:

發(fā)生擴(kuò)容

resize的展開(kāi)過(guò)程如下:

  1. 開(kāi)辟一個(gè)新的table數(shù)組,大小是原來(lái)的2倍冈欢,即table[32]
  2. 如果當(dāng)前節(jié)點(diǎn)沒(méi)有哈希沖突歉铝,則直接重新計(jì)算該節(jié)點(diǎn)的table[]數(shù)組下標(biāo)位置并且放入
  3. 如果當(dāng)前節(jié)點(diǎn)是紅黑樹(shù),則執(zhí)行紅黑樹(shù)的split操作將紅黑樹(shù)拆成兩半凑耻,如果拆分后的大小小于了TREEIFY_THRESHOLD閾值的話太示,降級(jí)為鏈表
  4. 如果當(dāng)前節(jié)點(diǎn)是鏈表,則根據(jù)當(dāng)前節(jié)點(diǎn)的hash & oldTable.size(如本例中為16)香浩,根據(jù)結(jié)果為0(low鏈表)或1(high鏈表)拆分為兩個(gè)鏈表类缤,也就是根據(jù)16的二進(jìn)制位(1_0000)從右往左數(shù)第5位
  • low鏈表,即節(jié)點(diǎn).hash & 16 == 0邻吭,即節(jié)點(diǎn).hash第5位為0的節(jié)點(diǎn)保持原下標(biāo)
  • high鏈表餐弱,即節(jié)點(diǎn).hash & 16 == 1,即節(jié)點(diǎn).hash第5位為1的節(jié)點(diǎn)下標(biāo)=原下標(biāo)+16

下圖展示了擴(kuò)容時(shí)鏈表的大致拆分過(guò)程


擴(kuò)容的過(guò)程

HashMap的modCount以及使用注意

如前文所述,HashMap內(nèi)部維護(hù)著一個(gè)成員變量modCount膏蚓,在HashMap每次進(jìn)行可能影響HashMap結(jié)構(gòu)的操作時(shí)都會(huì)導(dǎo)致modCount++(例如put瓢谢、remove)
這樣做是因?yàn)镠ashMap是不支持線程安全的,如果你的代碼在遍歷HashMap的同時(shí)又在修改影響著HashMap的內(nèi)容驮瞧,必然會(huì)導(dǎo)致遍歷出的結(jié)果不正確氓扛,與其拿到不正確結(jié)果導(dǎo)致后續(xù)基于這個(gè)錯(cuò)誤結(jié)果的一系列錯(cuò)誤,不如快速掀桌子拋出異常ConcurrentModificationException結(jié)束這次遍歷剧董,這個(gè)就是快速失敶鄙小(FailFast),其它相關(guān)的異常容錯(cuò)機(jī)制還有failover(失效轉(zhuǎn)移)翅楼、failback(失效自動(dòng)回復(fù))等,感興趣的讀者可以自行搜索真慢。

快速失敗-掀桌子

通過(guò)上文可以知道毅臊,modCount主要是遍歷請(qǐng)求受到影響時(shí)的處理方式,但是我們的代碼中經(jīng)常會(huì)遇到一種經(jīng)典的執(zhí)行情況铸史,如下

for(K key : map.keySet()) {
    if(key == xxx)
        map.remove(key); // 執(zhí)行完后下一輪for循環(huán)拋出ConcurrentModificationException
}

這種時(shí)候我們是在一個(gè)單線程中换淆,我們的目的是刪掉符合判斷條件的節(jié)點(diǎn)然后繼續(xù)遍歷Map袁翁,但是這樣會(huì)導(dǎo)致代碼拋出ConcurrentModificationException異常,就是因?yàn)樵趫?zhí)行了map.remove(key)之后modCount++蚯撩,進(jìn)而導(dǎo)致遍歷開(kāi)始前記錄的 expectModCount != modCount,從而拋出異常
解決的辦法是通過(guò)iterator.remove()刪除節(jié)點(diǎn)

Iterator iter = map.entrySet().iterator();
while(iter.hasNext()) {
    Entry entry = iter.next();
    if(entry.key == xxx)
        iter.remove();
}

HashMap的keySet和entrySet

在我們?nèi)粘i_(kāi)發(fā)中經(jīng)常需要對(duì)HashMap進(jìn)行遍歷烛占,常用遍歷方式有兩種:keySet()和entrySet()胎挎。
這兩種遍歷方式返回的Set集合本質(zhì)上都是HashMap的一個(gè)視圖,這個(gè)Set本身是不存儲(chǔ)數(shù)據(jù)的忆家,只是它覆寫(xiě)了相關(guān)的iterator犹菇、contains等方法,這些方法又會(huì)去對(duì)HashMap中的table數(shù)組進(jìn)行相關(guān)的查詢等操作芽卿。
當(dāng)我們對(duì)keySet()和entrySet()返回的Set集合進(jìn)行add操作時(shí)會(huì)拋出UnsupportedOperationException揭芍。

keySet與entrySet

高性能的并發(fā)哈希表--ConcurrentHashMap

以上討論的HashMap是JDK在Hashtable的改進(jìn)上實(shí)現(xiàn)了高性能的單線程版的哈希實(shí)現(xiàn),這在我們?nèi)粘F鋵?shí)已經(jīng)能夠處理很多場(chǎng)景卸例,甚至于當(dāng)你所需的HashMap需要實(shí)現(xiàn)線程隔離的時(shí)候也可以通過(guò)ThreadLocal來(lái)實(shí)現(xiàn)(詳見(jiàn) 圖解分析ThreadLocal的原理與應(yīng)用場(chǎng)景

但是某些場(chǎng)景的哈希表不得不在多個(gè)線程之間共享称杨,這些線程有可能同時(shí)讀某一個(gè)key,同時(shí)改某一個(gè)key筷转,一個(gè)在讀某個(gè)key的時(shí)候另一個(gè)卻在改這個(gè)key姑原,面對(duì)這種情況HashMap只能掀桌子了,但是我們總還是需要一種支持多線程的高效的哈希數(shù)據(jù)結(jié)構(gòu)旦装,

ConcurrentHashMap:“沒(méi)錯(cuò)页衙,正式在下”。

關(guān)于ConcurrentHashMap的高并發(fā)哈希實(shí)現(xiàn)原理會(huì)在下篇文章分析。

reference

[1] JDK8源碼
[2] 《算法導(dǎo)論》

本文的分析都是基于JDK8

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末店乐,一起剝皮案震驚了整個(gè)濱河市艰躺,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌眨八,老刑警劉巖腺兴,帶你破解...
    沈念sama閱讀 218,451評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異廉侧,居然都是意外死亡页响,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門段誊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)闰蚕,“玉大人,你說(shuō)我怎么就攤上這事连舍∶欢福” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,782評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵索赏,是天一觀的道長(zhǎng)盼玄。 經(jīng)常有香客問(wèn)我,道長(zhǎng)潜腻,這世上最難降的妖魔是什么埃儿? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,709評(píng)論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮融涣,結(jié)果婚禮上童番,老公的妹妹穿的比我還像新娘。我一直安慰自己暴心,他們只是感情好妓盲,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,733評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著专普,像睡著了一般悯衬。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上檀夹,一...
    開(kāi)封第一講書(shū)人閱讀 51,578評(píng)論 1 305
  • 那天筋粗,我揣著相機(jī)與錄音,去河邊找鬼炸渡。 笑死娜亿,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的蚌堵。 我是一名探鬼主播买决,決...
    沈念sama閱讀 40,320評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼沛婴,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了督赤?” 一聲冷哼從身側(cè)響起嘁灯,我...
    開(kāi)封第一講書(shū)人閱讀 39,241評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎躲舌,沒(méi)想到半個(gè)月后丑婿,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,686評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡没卸,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,878評(píng)論 3 336
  • 正文 我和宋清朗相戀三年羹奉,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片约计。...
    茶點(diǎn)故事閱讀 39,992評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡诀拭,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出病蛉,到底是詐尸還是另有隱情炫加,我是刑警寧澤,帶...
    沈念sama閱讀 35,715評(píng)論 5 346
  • 正文 年R本政府宣布铺然,位于F島的核電站,受9級(jí)特大地震影響酒甸,放射性物質(zhì)發(fā)生泄漏魄健。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,336評(píng)論 3 330
  • 文/蒙蒙 一插勤、第九天 我趴在偏房一處隱蔽的房頂上張望沽瘦。 院中可真熱鬧,春花似錦农尖、人聲如沸析恋。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,912評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)助隧。三九已至,卻和暖如春滑沧,著一層夾襖步出監(jiān)牢的瞬間并村,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,040評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工滓技, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留哩牍,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,173評(píng)論 3 370
  • 正文 我出身青樓令漂,卻偏偏與公主長(zhǎng)得像膝昆,于是被迫代替她去往敵國(guó)和親丸边。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,947評(píng)論 2 355