HashMap 在高并發(fā)下引起的死循環(huán)

HashMap 基本實現(xiàn)(JDK 8 之前)

HashMap 通常會用一個指針數(shù)組(假設(shè)為 table[])來做分散所有的 key膘婶,當(dāng)一個 key 被加入時悬襟,會通過 Hash 算法通過 key 算出這個數(shù)組的下標(biāo) i,然后就把這個 <key, value> 插到 table[i] 中脊岳,如果有兩個不同的 key 被算在了同一個 i,那么就叫沖突割捅,又叫碰撞,這樣會在 table[i] 上形成一個鏈表

如果 table[] 的尺寸很小亿驾,比如只有 2 個,如果要放進(jìn) 10 個 keys 的話莫瞬,那么碰撞非常頻繁,于是一個 O(1) 的查找算法乏悄,就變成了鏈表遍歷恳不,性能變成了 O(n),這是 Hash 表的缺陷

所以烟勋,Hash 表的尺寸和容量非常的重要。一般來說卵惦,Hash 表這個容器當(dāng)有數(shù)據(jù)要插入時,都會檢查容量有沒有超過設(shè)定的 threshold沮尿,如果超過,需要增大 Hash 表的尺寸畜疾,但是這樣一來,整個 Hash 表里的元素都需要被重算一遍啡捶。這叫 rehash,這個成本相當(dāng)?shù)拇?/p>

HashMap 的 rehash 源代碼

Put 一個 Key, Value 對到 Hash 表中:

public V put(K key, V value) {
    ......
    // 計算 Hash 值
    int hash = hash(key.hashCode());
    int i = indexFor(hash, table.length);
    // 如果該 key 已被插入瞎暑,則替換掉舊的 value(鏈接操作)
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    // 該 key 不存在,需要增加一個結(jié)點
    addEntry(hash, key, value, i);
    return null;
}

檢查容量是否超標(biāo)

void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
    //查看當(dāng)前的 size 是否超過了設(shè)定的閾值 threshold墨榄,如果超過,需要 resize
    if (size++ >= threshold)
        resize(2 * table.length);
}

新建一個更大尺寸的 hash 表渠概,然后把數(shù)據(jù)從老的 Hash 表中遷移到新的 Hash 表中

void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    ......
    // 創(chuàng)建一個新的 Hash Table
    Entry[] newTable = new Entry[newCapacity];
    // 將 Old Hash Table 上的數(shù)據(jù)遷移到 New Hash Table 上
    transfer(newTable);
    table = newTable;
    threshold = (int)(newCapacity * loadFactor);
}

遷移的源代碼

void transfer(Entry[] newTable) {
    Entry[] src = table;
    int newCapacity = newTable.length;
    //下面這段代碼的意思是:
    //  從OldTable里摘一個元素出來,然后放到NewTable中
    for (int j = 0; j < src.length; j++) {
        Entry<K,V> e = src[j];
        if (e != null) {
            src[j] = null;
            do {
                Entry<K,V> next = e.next;
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}

該方法實現(xiàn)的機(jī)制就是將每個鏈表轉(zhuǎn)化到新鏈表贮喧,并且鏈表中的位置發(fā)生反轉(zhuǎn),而這在多線程情況下是很容易造成鏈表回路箱沦,從而發(fā)生 get() 死循環(huán)。所以只要保證建新鏈時還是按照原來的順序的話就不會產(chǎn)生循環(huán)(JDK 8 的改進(jìn))

正常的 ReHash 的過程

  • 假設(shè)我們的 hash 算法就是簡單的用 key mod 一下表的大形叫巍(也就是數(shù)組的長度)
  • 最上面的是 old hash 表,其中的 Hash 表的 size = 2疆前,所以 key = 3, 7, 5,在 mod 2 以后都沖突在 table[1] 這里了
  • 接下來的三個步驟是 Hash 表 resize 成 4竹椒,然后所有的 <key, value> 重新 rehash 的過程

并發(fā)下的 Rehash

1)假設(shè)有兩個線程

do {
    Entry<K,V> next = e.next; //  假設(shè)線程一執(zhí)行到這里就被調(diào)度掛起了
    int i = indexFor(e.hash, newCapacity);
    e.next = newTable[i];
    newTable[i] = e;
    e = next;
} while (e != null);

而線程二執(zhí)行完成了。于是有下面的這個樣子



注意书释,因為 Thread1 的 e 指向了 key(3)赊窥,而 next 指向了 key(7),其在線程二 rehash 后锨能,指向了線程二重組后的鏈表≈酚觯可以看到鏈表的順序被反轉(zhuǎn)

2)線程一被調(diào)度回來執(zhí)行

  • 先是執(zhí)行 newTalbe[i] = e;
  • 然后是 e = next,導(dǎo)致了 e 指向了 key(7)
  • 而下一次循環(huán)的 next = e.next 導(dǎo)致了 next 指向了 key(3)

3)線程一接著工作傲隶。把 key(7) 摘下來,放到 newTable[i] 的第一個跺株,然后把 e 和 next 往下移


4)環(huán)形鏈接出現(xiàn)
e.next = newTable[i] 導(dǎo)致 key(3).next 指向了 key(7)
此時的 key(7).next 已經(jīng)指向了 key(3), 環(huán)形鏈表就這樣出現(xiàn)了


JDK 8 的改進(jìn)

JDK 8 中采用的是位桶 + 鏈表/紅黑樹的方式乒省,當(dāng)某個位桶的鏈表的長度超過 8 的時候,這個鏈表就將轉(zhuǎn)換成紅黑樹

HashMap 不會因為多線程 put 導(dǎo)致死循環(huán)(JDK 8 用 head 和 tail 來保證鏈表的順序和之前一樣袖扛;JDK 7 rehash 會倒置鏈表元素)十籍,但是還會有數(shù)據(jù)丟失等弊端(并發(fā)本身的問題)唇礁。因此多線程情況下還是建議使用 ConcurrentHashMap

為什么線程不安全

HashMap 在并發(fā)時可能出現(xiàn)的問題主要是兩方面:

  • 如果多個線程同時使用 put 方法添加元素,而且假設(shè)正好存在兩個 put 的 key 發(fā)生了碰撞(根據(jù) hash 值計算的 bucket 一樣)盏筐,那么根據(jù) HashMap 的實現(xiàn),這兩個 key 會添加到數(shù)組的同一個位置琢融,這樣最終就會發(fā)生其中一個線程 put 的數(shù)據(jù)被覆蓋

  • 如果多個線程同時檢測到元素個數(shù)超過數(shù)組大小 * loadFactor,這樣就會發(fā)生多個線程同時對 Node 數(shù)組進(jìn)行擴(kuò)容漾抬,都在重新計算元素位置以及復(fù)制數(shù)據(jù),但是最終只有一個線程擴(kuò)容后的數(shù)組會賦給 table纳令,也就是說其他線程的都會丟失她混,并且各自線程 put 的數(shù)據(jù)也丟失

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市毯欣,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌酗钞,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件砚作,死亡現(xiàn)場離奇詭異,居然都是意外死亡葫录,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進(jìn)店門米同,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人面粮,你說我怎么就攤上這事少孝∩宰撸” “怎么了?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵婿脸,是天一觀的道長。 經(jīng)常有香客問我盖淡,道長,這世上最難降的妖魔是什么褪迟? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮味赃,結(jié)果婚禮上掀抹,老公的妹妹穿的比我還像新娘傲武。我一直安慰自己,他們只是感情好城榛,可當(dāng)我...
    茶點故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著狠持,像睡著了一般。 火紅的嫁衣襯著肌膚如雪喘垂。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天正勒,我揣著相機(jī)與錄音,去河邊找鬼章贞。 笑死,一個胖子當(dāng)著我的面吹牛鸭限,可吹牛的內(nèi)容都是我干的就谜。 我是一名探鬼主播丧荐,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼虹统!你這毒婦竟也來了弓坞?” 一聲冷哼從身側(cè)響起车荔,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎忧便,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體珠增,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年蒂教,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片凝垛。...
    茶點故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖梦皮,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情剑肯,我是刑警寧澤捧毛,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站型将,受9級特大地震影響寂祥,放射性物質(zhì)發(fā)生泄漏七兜。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一腕铸、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧狠裹,春花似錦、人聲如沸涛菠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽礁叔。三九已至,卻和暖如春琅关,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背涣易。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留都毒,地道東北人色罚。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓戳护,卻偏偏與公主長得像,于是被迫代替她去往敵國和親瀑焦。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,601評論 2 353

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