程序員之Map

HashMap痢法、HashTable、ConcurrentHashMap

a.線程安全問題
HashMap是線程不安全的,多線程環(huán)境下可能會導致死循環(huán)(HashMap擴容時)锄列,key可以為null;
在jdk1.7中惯悠,HashMap底層是通過“數(shù)組 + 鏈表”的實現(xiàn)方式邻邮,put使用頭插法(多線程情況下擴容時可能會出現(xiàn)鏈表環(huán));
在jdk1.8中克婶,HashMap底層是通過“數(shù)組+鏈表+紅黑樹”的方式筒严,當鏈表節(jié)點較少時(<=8),使用鏈表鸠补,當鏈表節(jié)點較多時(>8且size()>64萝风,當size小于64的時候會執(zhí)行擴容)轉(zhuǎn)為紅黑樹,put使用尾插法紫岩。
數(shù)據(jù)不一致:
當多個線程同時想hashMap中put數(shù)據(jù)時规惰,當出現(xiàn)一個以上的線程put的數(shù)據(jù)的坐標一致時,可能會導致一個線程的數(shù)據(jù)修改被覆蓋泉蝌,導致數(shù)據(jù)不一致歇万。原理如同數(shù)據(jù)庫的事務,數(shù)據(jù)被覆蓋勋陪。
死循環(huán):
在HasMap擴容時贪磺,多線程的擴容會導致在rehash鏈表的時候,有可能會導致死循環(huán)诅愚。

HashMap的put方法
1.首先 Put 方法接收到 key 和 value 時寒锚,會先利用 key 進行哈希算法得到這個 key 對應的值Hash。
2.再通過這個哈希值與數(shù)組長度-1進行與操作得到一個數(shù)組下標
3..再判斷數(shù)組下標位置是不是空著违孝,如果空著刹前,則直接把 key 和 value 封裝為一個 Node 對象并存入此數(shù)組位置。
4.如果此下標位置上非空雌桑,表示此位置上存在 Node 對象喇喉,那么則判斷該 Node 對象是不是一個紅黑樹節(jié)點,如果是則將 key 和 value 封裝為一個紅黑樹節(jié)點并添加到紅黑樹中去校坑,在這個過程中會判斷紅黑樹中是否存在當前 key 拣技,如果存在則更新value千诬。
5.如果此位置上的 Node 對象是鏈表節(jié)點,則將 key 和 value 封裝為一個鏈表 Node 并插入到鏈表中去膏斤。
6.插入到鏈表中后徐绑,會判斷鏈表的節(jié)點個數(shù)是不是超過了8個,如果超過則把當前位置的鏈表轉(zhuǎn)化為紅黑樹掸绞。
7.插入鏈表使用的是尾插法泵三,所以需要遍歷鏈表,而在這個過程中也會去判斷 key 是否存在衔掸,如果存在則更新 value烫幕。

b.實現(xiàn)原理
HashTable與HashMap實現(xiàn)原理是一樣的,但是HashTable的get和put方法都是synchronized操作敞映,不允許key和value為null较曼,因此HashTable的性能是比較差的。
總結:

名稱 默認大小 負載因子 存儲方式 擴容大小 擴容條件 線程安全 key可否為null
HashMap 16 0.75 數(shù)組+鏈表(紅黑樹) 原來的2倍 負載數(shù)>=負載因子 * 當前大小 可以
HashTable 11 0.75 數(shù)組+鏈表(紅黑樹) 原來的2倍+1 負載數(shù)>=負載因子 * 當前大小 不可以

c.ConcurrentHashMap
ConcurrentHashMap避免了HashTable對全局加鎖改成了局部加鎖操作振愿,這樣就極大地提高了并發(fā)環(huán)境下的操作速度捷犹。
在jdk1.7中,ConcurrentHashMap采用了“數(shù)組 + Segment + 分段鎖”的方式實現(xiàn)冕末,其中每個Segment內(nèi)部是一個Entry數(shù)組萍歉,數(shù)組中的每個元素又是一個鏈表,ConcurrentHashMap在每個Segment上加鎖档桃,這樣當一個線程占用鎖訪問其中一個段數(shù)據(jù)的時候枪孩,其他段的數(shù)據(jù)也能被其他線程訪問,能夠?qū)崿F(xiàn)真正的并發(fā)訪問藻肄,實現(xiàn)了粗粒度的分段鎖蔑舞。

ConcurrentHashMap內(nèi)部結構圖.jpg

ConcurrentHashMap訪問一個數(shù)據(jù)時,需要兩次Hash操作嘹屯,第一次Hash定位到Segment攻询,第二次Hash定位到元素所在鏈表的頭部。因此這也是Segment方案的查詢性能比較慢的原因州弟。
在jdk1.8中钧栖,ConcurrentHashMap參考了JDK8 HashMap的實現(xiàn),采用了“數(shù)組+鏈表+紅黑樹“”來實現(xiàn)婆翔。同時拋棄了Segment轉(zhuǎn)而采用的是Node桐经,每個node保存有key、value和key的Hash值浙滤,其中value和next都是用volatile修飾,保證并發(fā)可見气堕。
ConcurrentHashMap內(nèi)部大量采用CAS(compare and swap)操作纺腊,CAS 操作包含三個操作數(shù) —— 內(nèi)存位置(V)畔咧、預期原值(A)和新值(B)。如果內(nèi)存地址里面的值和A的值是一樣的揖膜,那么就將內(nèi)存里面的值更新成B誓沸。CAS是通過無限循環(huán)來獲取數(shù)據(jù)的,若果在第一輪循環(huán)中壹粟,a線程獲取地址里面的值被b線程修改了拜隧,那么a線程需要自旋,到下次循環(huán)才有可能機會執(zhí)行趁仙。
put加鎖位置
put的過程如下:
1.當存放數(shù)據(jù)的Node<K,V>[] table數(shù)組為null或者length=0的時候初始化table數(shù)組洪添;
2.當key的hash值對應的Node<K,V>[] table數(shù)組的坐標值為null的時候,使用CAS進行賦值雀费;
3.當Node<K,V>[] table數(shù)組正在擴容干奢,那么當前線程參與擴容,使用helpTransfer方法盏袄;
4.否則對key的hash值對應的Node<K,V>[] table數(shù)組的坐標對象進行synchronized加鎖忿峻,如果是鏈表,那么使用尾插法進行插入辕羽;如果是紅黑樹逛尚,則插入紅黑樹;

加鎖的位置是在Node<K,V>[] table數(shù)組中的節(jié)點刁愿,即不會加鎖到鏈表的非head節(jié)點绰寞,也不會加鎖的紅黑樹的非根節(jié)點。

擴容問題
jdk1.7的HashMap擴容
jdk1.8的HashMap擴容
jdk1.7的ConcurrentHashMap擴容
jdk1.8的ConcurrentHashMap擴容
ConcurrentHashMap擴容時酌毡,按照正常的邏輯所有的讀寫都要阻塞克握,但是大牛就是神一樣的存在,Doug lea(膜拜一下)對ConcurrentHashMap擴容做了優(yōu)化枷踏,具體思路是:在ConcurrentHashMap擴容時菩暗,如果有讀寫線程進來,那么可以讓這些讀寫線程參與到擴容中旭蠕,這樣加快了擴容停团,而且讀寫線程也不需要始終等待。

ConcurrentHashMap引入了ForwardingNode類掏熬,當線程發(fā)起擴容時佑稠,就會更改sizeCtl的值

    /**
     * Table initialization and resizing control.  When negative, the
     * table is being initialized or resized: -1 for initialization,
     * else -(1 + the number of active resizing threads).  Otherwise,
     * when table is null, holds the initial table size to use upon
     * creation, or 0 for default. After initialization, holds the
     * next element count value upon which to resize the table.
     */
    private transient volatile int sizeCtl;

對于擴容時的讀操作
如果當前節(jié)點有數(shù)據(jù),還沒有遷移走旗芬,則可以直接讀舌胶,不影響;
如果當前節(jié)點已經(jīng)遷移走疮丛,那么都節(jié)點會設置成fwd節(jié)點幔嫂,此時讀線程會參與到擴容中辆它。

對于擴容時的寫或者刪除操作
如果當前節(jié)點已經(jīng)遷移走,那么都節(jié)點會設置成fwd節(jié)點履恩,此時讀線程會參與到擴容中锰茉;
如果當前節(jié)點有數(shù)據(jù),還沒有遷移走切心,當前鏈表的頭結點會被鎖住飒筑,寫或者刪除操作會被阻塞,直到擴容完成绽昏。

總結如下:

序號 特征 jdk1.7 ConcurrentHashMap jdk1.8 ConcurrentHashMap
1 數(shù)據(jù)結構 Segment 方式 數(shù)組+鏈表+紅黑樹的結構
2 線程安全機制 采用segment的分段鎖機制 CAS+Synchronized保證線程安全
3 鎖的粒度 Segment加鎖 每個數(shù)組元素加鎖(Node)
4 Hash沖突處理 鏈表 鏈表 + 紅黑樹(節(jié)點數(shù)大于8時)
5 查詢時間復雜度 遍歷鏈表O(n) 遍歷紅黑樹O(logN)
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末协屡,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子而涉,更是在濱河造成了極大的恐慌著瓶,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件啼县,死亡現(xiàn)場離奇詭異材原,居然都是意外死亡,警方通過查閱死者的電腦和手機季眷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門余蟹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人子刮,你說我怎么就攤上這事威酒。” “怎么了挺峡?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵葵孤,是天一觀的道長。 經(jīng)常有香客問我橱赠,道長尤仍,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任狭姨,我火速辦了婚禮宰啦,結果婚禮上,老公的妹妹穿的比我還像新娘饼拍。我一直安慰自己赡模,他們只是感情好,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布师抄。 她就那樣靜靜地躺著漓柑,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上欺缘,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天栋豫,我揣著相機與錄音,去河邊找鬼谚殊。 笑死,一個胖子當著我的面吹牛蛤铜,可吹牛的內(nèi)容都是我干的嫩絮。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼围肥,長吁一口氣:“原來是場噩夢啊……” “哼剿干!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起穆刻,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤置尔,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后氢伟,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體榜轿,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年朵锣,在試婚紗的時候發(fā)現(xiàn)自己被綠了谬盐。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片勇吊。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡蔓肯,死狀恐怖吞滞,靈堂內(nèi)的尸體忽然破棺而出缚俏,到底是詐尸還是另有隱情婚陪,我是刑警寧澤溪胶,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布甚纲,位于F島的核電站听盖,受9級特大地震影響绞吁,放射性物質(zhì)發(fā)生泄漏幢痘。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一掀泳、第九天 我趴在偏房一處隱蔽的房頂上張望雪隧。 院中可真熱鬧,春花似錦员舵、人聲如沸脑沿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽庄拇。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間措近,已是汗流浹背溶弟。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留瞭郑,地道東北人辜御。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像屈张,于是被迫代替她去往敵國和親擒权。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

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