老生常談,HashMap的死循環(huán)

占小狼 轉載請注明原創(chuàng)出處旺韭,謝謝氛谜!

問題

最近的幾次面試中,我都問了是否了解HashMap在并發(fā)使用時可能發(fā)生死循環(huán)区端,導致cpu100%值漫,結果讓我很意外,都表示不知道有這樣的問題织盼,讓我意外的是面試者的工作年限都不短杨何。

由于HashMap并非是線程安全的,所以在高并發(fā)的情況下必然會出現(xiàn)問題沥邻,這是一個普遍的問題危虱,雖然網上分析的文章很多,還是覺得有必須寫一篇文章谋国,讓關注我公眾號的同學能夠意識到這個問題槽地,并了解這個死循環(huán)是如何產生的。

如果是在單線程下使用HashMap芦瘾,自然是沒有問題的,如果后期由于代碼優(yōu)化集畅,這段邏輯引入了多線程并發(fā)執(zhí)行近弟,在一個未知的時間點,會發(fā)現(xiàn)CPU占用100%挺智,居高不下祷愉,通過查看堆棧,你會驚訝的發(fā)現(xiàn),線程都Hang在hashMap的get()方法上二鳄,服務重啟之后赴涵,問題消失,過段時間可能又復現(xiàn)了订讼。

這是為什么髓窜?

原因分析

在了解來龍去脈之前,我們先看看HashMap的數(shù)據結構欺殿。

在內部寄纵,HashMap使用一個Entry數(shù)組保存key、value數(shù)據脖苏,當一對key程拭、value被加入時,會通過一個hash算法得到數(shù)組的下標index棍潘,算法很簡單恃鞋,根據key的hash值,對數(shù)組的大小取模 hash & (length-1)亦歉,并把結果插入數(shù)組該位置山宾,如果該位置上已經有元素了,就說明存在hash沖突鳍徽,這樣會在index位置生成鏈表资锰。

如果存在hash沖突,最慘的情況阶祭,就是所有元素都定位到同一個位置绷杜,形成一個長長的鏈表,這樣get一個值時濒募,最壞情況需要遍歷所有節(jié)點鞭盟,性能變成了O(n),所以元素的hash值算法和HashMap的初始化大小很重要瑰剃。

當插入一個新的節(jié)點時齿诉,如果不存在相同的key,則會判斷當前內部元素是否已經達到閾值(默認是數(shù)組大小的0.75)晌姚,如果已經達到閾值粤剧,會對數(shù)組進行擴容,也會對鏈表中的元素進行rehash挥唠。

實現(xiàn)

HashMap的put方法實現(xiàn):

1抵恋、判斷key是否已經存在

public V put(K key, V value) {
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key);
    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不存在弧关,則插入新的元素
    addEntry(hash, key, value, i);
    return null;
}

2盅安、檢查容量是否達到閾值threshold

void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);
}

如果元素個數(shù)已經達到閾值,則擴容世囊,并把原來的元素移動過去别瞭。

3、擴容實現(xiàn)

void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    ...

    Entry[] newTable = new Entry[newCapacity];
    ...
    transfer(newTable, rehash);
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

這里會新建一個更大的數(shù)組株憾,并通過transfer方法蝙寨,移動元素。

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}

移動的邏輯也很清晰号胚,遍歷原來table中每個位置的鏈表籽慢,并對每個元素進行重新hash,在新的newTable找到歸宿猫胁,并插入箱亿。

案例分析

假設HashMap初始化大小為4,插入個3節(jié)點弃秆,不巧的是届惋,這3個節(jié)點都hash到同一個位置,如果按照默認的負載因子的話菠赚,插入第3個節(jié)點就會擴容脑豹,為了驗證效果,假設負載因子是1.

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}

以上是節(jié)點移動的相關邏輯衡查。

插入第4個節(jié)點時瘩欺,發(fā)生rehash,假設現(xiàn)在有兩個線程同時進行拌牲,線程1和線程2俱饿,兩個線程都會新建新的數(shù)組。

假設 線程2 在執(zhí)行到Entry<K,V> next = e.next;之后塌忽,cpu時間片用完了拍埠,這時變量e指向節(jié)點a,變量next指向節(jié)點b土居。

線程1繼續(xù)執(zhí)行枣购,很不巧,a擦耀、b棉圈、c節(jié)點rehash之后又是在同一個位置7,開始移動節(jié)點

第一步埂奈,移動節(jié)點a

第二步迄损,移動節(jié)點b

注意惰瓜,這里的順序是反過來的妈经,繼續(xù)移動節(jié)點c

這個時候 線程1 的時間片用完,內部的table還沒有設置成新的newTable肩榕, 線程2 開始執(zhí)行垮抗,這時內部的引用關系如下:

這時氏捞,在 線程2 中,變量e指向節(jié)點a冒版,變量next指向節(jié)點b液茎,開始執(zhí)行循環(huán)體的剩余邏輯。

Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;

執(zhí)行之后的引用關系如下圖

執(zhí)行后辞嗡,變量e指向節(jié)點b捆等,因為e不是null,則繼續(xù)執(zhí)行循環(huán)體续室,執(zhí)行后的引用關系

變量e又重新指回節(jié)點a栋烤,只能繼續(xù)執(zhí)行循環(huán)體,這里仔細分析下:
1挺狰、執(zhí)行完Entry<K,V> next = e.next;明郭,目前節(jié)點a沒有next,所以變量next指向null丰泊;
2薯定、e.next = newTable[i]; 其中 newTable[i] 指向節(jié)點b,那就是把a的next指向了節(jié)點b瞳购,這樣a和b就相互引用了话侄,形成了一個環(huán);
3学赛、newTable[i] = e 把節(jié)點a放到了數(shù)組i位置年堆;
4、e = next; 把變量e賦值為null罢屈,因為第一步中變量next就是指向null嘀韧;

所以最終的引用關系是這樣的:

節(jié)點a和b互相引用,形成了一個環(huán)缠捌,當在數(shù)組該位置get尋找對應的key時锄贷,就發(fā)生了死循環(huán)。

另外曼月,如果線程2把newTable設置成到內部的table谊却,節(jié)點c的數(shù)據就丟了,看來還有數(shù)據遺失的問題哑芹。

總結

所以在并發(fā)的情況炎辨,發(fā)生擴容時,可能會產生循環(huán)鏈表聪姿,在執(zhí)行get的時候碴萧,會觸發(fā)死循環(huán)乙嘀,引起CPU的100%問題,所以一定要避免在并發(fā)環(huán)境下使用HashMap破喻。

曾經有人把這個問題報給了Sun虎谢,不過Sun不認為這是一個bug,因為在HashMap本來就不支持多線程使用曹质,要并發(fā)就用ConcurrentHashmap婴噩。

END。
我是占小狼羽德。 如果讀完覺得有收獲的話几莽,記得關注和點贊

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市宅静,隨后出現(xiàn)的幾起案子章蚣,更是在濱河造成了極大的恐慌,老刑警劉巖坏为,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件究驴,死亡現(xiàn)場離奇詭異,居然都是意外死亡匀伏,警方通過查閱死者的電腦和手機洒忧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來够颠,“玉大人熙侍,你說我怎么就攤上這事÷哪ィ” “怎么了蛉抓?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長剃诅。 經常有香客問我巷送,道長,這世上最難降的妖魔是什么矛辕? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任笑跛,我火速辦了婚禮,結果婚禮上聊品,老公的妹妹穿的比我還像新娘飞蹂。我一直安慰自己,他們只是感情好翻屈,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布陈哑。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪惊窖。 梳的紋絲不亂的頭發(fā)上刽宪,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天,我揣著相機與錄音爬坑,去河邊找鬼纠屋。 笑死涂臣,一個胖子當著我的面吹牛盾计,可吹牛的內容都是我干的。 我是一名探鬼主播赁遗,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼署辉,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了岩四?” 一聲冷哼從身側響起哭尝,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎剖煌,沒想到半個月后材鹦,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡耕姊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年桶唐,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片茉兰。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡尤泽,死狀恐怖,靈堂內的尸體忽然破棺而出规脸,到底是詐尸還是另有隱情坯约,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布莫鸭,位于F島的核電站闹丐,受9級特大地震影響,放射性物質發(fā)生泄漏被因。R本人自食惡果不足惜卿拴,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望氏身。 院中可真熱鬧巍棱,春花似錦、人聲如沸蛋欣。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽陷虎。三九已至到踏,卻和暖如春杠袱,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背窝稿。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工楣富, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人伴榔。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓纹蝴,卻偏偏與公主長得像,于是被迫代替她去往敵國和親踪少。 傳聞我的和親對象是個殘疾皇子塘安,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內容

  • Java8張圖 11、字符串不變性 12援奢、equals()方法兼犯、hashCode()方法的區(qū)別 13、...
    Miley_MOJIE閱讀 3,693評論 0 11
  • 一、基本數(shù)據類型 注釋 單行注釋:// 區(qū)域注釋:/* */ 文檔注釋:/** */ 數(shù)值 對于byte類型而言...
    龍貓小爺閱讀 4,254評論 0 16
  • 5.1具篇、對于HashMap需要掌握以下幾點 Map的創(chuàng)建:HashMap() 往Map中添加鍵值對:即put(Ob...
    rochuan閱讀 666評論 0 0
  • 是一顆孤伶的石頭 他以為 是一粒遙遠的星辰 夜了 一點熒熒 最不該 星零孕了詩心 詩心染了凡心 凡心動了春心 夢醒...
    夢之i閱讀 178評論 0 0