HashMap 1.8死鎖問題詳細(xì)分析

轉(zhuǎn)載至:https://www.cnblogs.com/xuwc/p/14044664.html
參考:

https://blog.csdn.net/liu_jm/article/details/105582711

https://blog.csdn.net/gs_albb/article/details/88091808

https://blog.csdn.net/qq_33330687/article/details/101479385

HashMap在jdk1.8也會(huì)出現(xiàn)死循環(huán)的問題(實(shí)測(cè))

昨天在面試的時(shí)候,面試官問到HashMap在多線程并發(fā)的情況下怎么出現(xiàn)死循環(huán)的破婆?
我當(dāng)時(shí)非常有底氣的回答:Hashmap在jdk1.8之前多線程同時(shí)進(jìn)行put操作,并且同時(shí)進(jìn)行擴(kuò)容的時(shí)候可能會(huì)出現(xiàn)鏈表環(huán)邪乍,導(dǎo)致死循環(huán)的發(fā)生宙帝。
面試官緊接著問:那1.8就不會(huì)出現(xiàn)死循環(huán)了嗎秤朗?
底氣瞬間消失丹莲,思考一會(huì):1.8在多線程的情況下HashMap會(huì)出現(xiàn)數(shù)據(jù)丟失的問題,比如…(給面試官舉了個(gè)栗子)朗涩。
面試官毫無(wú)波瀾忽孽,好像沒有g(shù)et到答案的樣子問:那jdk1.8的HashMap是不是就不會(huì)出現(xiàn)死循環(huán)了?
我當(dāng)時(shí)愣了一下谢床,心想難道1.8也會(huì)出現(xiàn)死循環(huán)…考慮一會(huì)兄一,最后決定堅(jiān)持自己,但是已經(jīng)沒有了底氣:是的识腿。
面試官露出了開心的樣子:嗯出革,好的。(看來(lái)肯定是說(shuō)錯(cuò)了)


介紹完前因后果渡讼,那么來(lái)看一下Hashmap在jdk1.8會(huì)不會(huì)出現(xiàn)死循環(huán)的情況骂束。

測(cè)試代碼

import java.util.*;
public class MainTest {
    Map<String,String> map = new HashMap<>();

    public void hashMapTest() {
        for (int i = 0;i < 500;i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0;j < 500;j++) {
                        map.put(Thread.currentThread().getName() + "-" + j, j+"");
                    }
                }
            }).start();
        }
        try {
            Thread.sleep(2000);
//            map.forEach((x,y) -> System.out.println(x));
            System.out.println(map.size());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

[圖片上傳失敗...(image-5e3108-1699429002669)]

發(fā)現(xiàn)cpu使用率99.9%,符合死循環(huán)的情況成箫。
然后再使用 jps 和 jstack 命令查看一下進(jìn)程的狀態(tài)
[圖片上傳失敗...(image-2d2df2-1699429002669)]

看一下HashMap的1948行

final void treeify(Node<K,V>[] tab) {
            TreeNode<K,V> root = null;
            for (TreeNode<K,V> x = this, next; x != null; x = next) {
                next = (TreeNode<K,V>)x.next;
                x.left = x.right = null;
                if (root == null) {
                    x.parent = null;
                    x.red = false;
                    root = x;
                }
                else {
                    K k = x.key;
                    int h = x.hash;
                    Class<?> kc = null;
                    //說(shuō)明線程在這個(gè)for循環(huán)中一直沒有返回展箱,導(dǎo)致了死循環(huán)
                    for (TreeNode<K,V> p = root;;) {
                        int dir, ph;
                        K pk = p.key;
                        if ((ph = p.hash) > h)
                            dir = -1;
                        else if (ph < h)
                            dir = 1;
                        else if ((kc == null &&
                                  (kc = comparableClassFor(k)) == null) ||
                                 (dir = compareComparables(kc, k, pk)) == 0)
                            dir = tieBreakOrder(k, pk);

                        TreeNode<K,V> xp = p;
                        if ((p = (dir <= 0) ? p.left : p.right) == null) {
                            x.parent = xp;
                            if (dir <= 0)
                                xp.left = x;
                            else
                                xp.right = x;
                            root = balanceInsertion(root, x);
                            break;
                        }
                    } // 1948行
                }
            }
            moveRootToFront(tab, root);
        }

這個(gè)方法看出,說(shuō)明Hashmap在jdk1.8的時(shí)候也會(huì)出現(xiàn)死循環(huán)的情況蹬昌,是在鏈表轉(zhuǎn)換為樹的時(shí)候for循環(huán)一直無(wú)法跳出析藕,導(dǎo)致死循環(huán)。
不止這些凳厢,經(jīng)過(guò)多次測(cè)試,發(fā)現(xiàn)死循環(huán)還會(huì)在2239行出現(xiàn)

java.lang.Thread.State: RUNNABLE
    at java.util.HashMap$TreeNode.balanceInsertion(HashMap.java:2239)
    at java.util.HashMap$TreeNode.treeify(HashMap.java:1945)
    at java.util.HashMap$TreeNode.split(HashMap.java:2180)
    at java.util.HashMap.resize(HashMap.java:714)
    at java.util.HashMap.putVal(HashMap.java:663)
    at java.util.HashMap.put(HashMap.java:612)
    at com.luck.ejob.server.MainTest$1.run(MainTest.java:151)
    at java.lang.Thread.run(Thread.java:748)

再看下面這種情況

java.lang.Thread.State: RUNNABLE
    at java.util.HashMap$TreeNode.root(HashMap.java:1824)
    at java.util.HashMap$TreeNode.putTreeVal(HashMap.java:1978)
    at java.util.HashMap.putVal(HashMap.java:638)
    at java.util.HashMap.put(HashMap.java:612)
    at com.luck.ejob.server.MainTest$1.run(MainTest.java:151)
    at java.lang.Thread.run(Thread.java:748)

final TreeNode<K,V> root() {
    for (TreeNode<K,V> r = this, p;;) {
        if ((p = r.parent) == null)
            return r;
        r = p; //1824行
    }
}

所以jdk1.8的HashMap在多線程的情況下也會(huì)出現(xiàn)死循環(huán)的問題竞慢,但是1.8是在鏈表轉(zhuǎn)換樹或者對(duì)樹進(jìn)行操作的時(shí)候會(huì)出現(xiàn)線程安全的問題先紫。

復(fù)習(xí)一下線程安全的定義:

在擁有共享數(shù)據(jù)的多條線程并行執(zhí)行的程序中,線程安全的代碼會(huì)通過(guò)同步機(jī)制保證各個(gè)線程都可以正常且正確的執(zhí)行筹煮,不會(huì)出現(xiàn)數(shù)據(jù)污染等意外情況遮精。

HashMap在jdk1.8中也會(huì)死循環(huán)

jdk1.7版本中多線程同時(shí)對(duì)HashMap擴(kuò)容時(shí),會(huì)引起鏈表死循環(huán)败潦,盡管jdk1.8修復(fù)了該問題本冲,但是同樣在jdk1.8版本中多線程操作hashMap時(shí)仍然會(huì)引起死循環(huán),只是原因不一樣劫扒。


示例代碼

package com.gsonkeno.interview;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
/**
 *
 * jdk7擴(kuò)容時(shí)都可能導(dǎo)致死鎖
 * jdk8在PutTreeValue時(shí)可能死循環(huán)   死循環(huán)在hashMap的1816行或2229行檬洞, java version "1.8.0_111"
 * jstack發(fā)現(xiàn)可能卡在 at java.util.HashMap$TreeNode.balanceInsertion(HashMap.java:2229)
 * 也有可能卡在  at java.util.HashMap$TreeNode.root(HashMap.java:1816)
 * @author gaosong
 * @since 2019-02-23
 */
public class HashMap1 {
    public static void main(String[] args) {
        HashMapThread hmt0 = new HashMapThread();
        HashMapThread hmt1 = new HashMapThread();
        HashMapThread hmt2 = new HashMapThread();
        HashMapThread hmt3 = new HashMapThread();
        HashMapThread hmt4 = new HashMapThread();
        hmt0.start();
        hmt1.start();
        hmt2.start();
        hmt3.start();
        hmt4.start();
    }
}

class HashMapThread extends Thread
{
    private static AtomicInteger ai = new AtomicInteger(0);
    private static Map<Integer, Integer> map = new HashMap<Integer, Integer>(1);

    @Override
    public void run()
    {
        while (ai.get() < 100000)
        {
            map.put(ai.get(), ai.get());
            ai.incrementAndGet();
        }
        System.out.println(Thread.currentThread().getName() + "執(zhí)行結(jié)束完");
    }
}

代碼見github

說(shuō)明

[圖片上傳失敗...(image-334c7d-1699429002669)]

多個(gè)線程全部全部在如下代碼塊的1816行,一直循環(huán)沟饥,無(wú)法退出for循環(huán)添怔。

        final TreeNode<K,V> root() {
            for (TreeNode<K,V> r = this, p;;) {
                if ((p = r.parent) == null)
                    return r;
                r = p;   //1816行
            }
        }

類似地湾戳,還有可能卡在at java.util.HashMap$TreeNode.balanceInsertion(HashMap.java:2229)
可能Node節(jié)點(diǎn)轉(zhuǎn)換為TreeNode結(jié)點(diǎn)異常

主要原因其實(shí)是多線程下操作同一對(duì)象時(shí),對(duì)象內(nèi)部屬性的不一致性導(dǎo)致的广料。分析方式跟為什么單例要使用雙重鎖check類似砾脑。

總結(jié)的經(jīng)驗(yàn)就是,在多線程下不要使用HashMap艾杏,至少jdk8及其以下不要使用韧衣,之上版本也建議不要使用。

JDK8中HashMap依然會(huì)死循環(huán)

JDK8中HashMap依然會(huì)死循環(huán)购桑!

是否你聽說(shuō)過(guò)JDK8之后HashMap已經(jīng)解決的擴(kuò)容死循環(huán)的問題畅铭,雖然HashMap依然說(shuō)線程不安全,但是不會(huì)造成服務(wù)器load飆升的問題其兴。

然而事實(shí)并非如此顶瞒。少年可曾了解一種紅黑樹成環(huán)的場(chǎng)景,=v=

今日在查看監(jiān)控時(shí)候發(fā)現(xiàn)元旬,某一臺(tái)機(jī)器load飆升
[圖片上傳失敗...(image-1e4e75-1699429002669)]

感覺問題不對(duì)勁榴徐,ssh大法登陸機(jī)器,top匀归,top -Hp,jstack坑资,jmap四連擊保存下來(lái)堆棧,cpu使用最高的線程穆端,內(nèi)存信息準(zhǔn)備分析袱贮。

首先查看使用最耗費(fèi)cpu的線程堆棧信息

cat stack | grep -i 34670 -C10 --color

[圖片上傳失敗...(image-e21a45-1699429002669)]

我勒個(gè)去,HashMap体啰,猜測(cè)八成死循環(huán)了攒巍,但是我們使用的JDK8,在8中通過(guò)棧封閉的鏈表替換荒勇,解決了擴(kuò)容死循環(huán)的問題柒莉。疑惑,繼續(xù)往下看沽翔。

根據(jù)堆棧信息兢孝,root方法是問題所在,點(diǎn)開HashMap源碼

[圖片上傳失敗...(image-6ae6eb-1699429002669)]

好嘛仅偎,load飆高跨蟹,代碼有個(gè)for語(yǔ)句,我覺得鐵定死循環(huán)了橘沥,看代碼情況只可能是兩個(gè)紅黑樹節(jié)點(diǎn)的父親節(jié)點(diǎn)相互引用才可以導(dǎo)致無(wú)法走出這個(gè)for語(yǔ)句窗轩。

然而這都是我的猜測(cè),我沒有證據(jù)座咆。而且讓我追紅黑樹的代碼品姓,也是需要耗費(fèi)大量時(shí)間的事情寝并,我需要快速驗(yàn)證我的猜測(cè)。

我之前dump下來(lái)了堆內(nèi)存信息腹备,我通過(guò)jhat 命令生成html的內(nèi)存信息頁(yè)面
[圖片上傳失敗...(image-56d23d-1699429002668)]

然后輸入http://localhost:7000查看

我先找業(yè)務(wù)代碼中持有這個(gè)HashMap的對(duì)象衬潦,然后點(diǎn)進(jìn)去查詢內(nèi)部信息

[圖片上傳失敗...(image-d12c47-1699429002668)]

因?yàn)閿?shù)據(jù)都放在table中,點(diǎn)擊Table字段植酥,查看其內(nèi)容

[圖片上傳失敗...(image-604f65-1699429002668)]

table中存在唯一的一個(gè)TreeNode節(jié)點(diǎn)镀岛,這肯定是已經(jīng)變成了紅黑樹了

點(diǎn)進(jìn)去查看

[圖片上傳失敗...(image-54a851-1699429002668)]

點(diǎn)擊parent字段信息

[圖片上傳失敗...(image-b6c931-1699429002668)]

0x72745d828與0x72745d7b8兩個(gè)TreeNode節(jié)點(diǎn)的Parent引用都是對(duì)方。

后續(xù)打算深入研究一下紅黑樹什么場(chǎng)景會(huì)造成這個(gè)原因友驮。

最后漂羊,無(wú)論什么并發(fā)場(chǎng)景請(qǐng)別使用HashMap,ConcurrentHashmap大法好

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末卸留,一起剝皮案震驚了整個(gè)濱河市走越,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌耻瑟,老刑警劉巖旨指,帶你破解...
    沈念sama閱讀 219,539評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異喳整,居然都是意外死亡谆构,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門框都,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)搬素,“玉大人,你說(shuō)我怎么就攤上這事魏保“境撸” “怎么了?”我有些...
    開封第一講書人閱讀 165,871評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵谓罗,是天一觀的道長(zhǎng)猪杭。 經(jīng)常有香客問我,道長(zhǎng)妥衣,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,963評(píng)論 1 295
  • 正文 為了忘掉前任戒傻,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘腰素。我一直安慰自己蛤织,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評(píng)論 6 393
  • 文/花漫 我一把揭開白布不翩。 她就那樣靜靜地躺著兵扬,像睡著了一般麻裳。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上器钟,一...
    開封第一講書人閱讀 51,763評(píng)論 1 307
  • 那天津坑,我揣著相機(jī)與錄音,去河邊找鬼傲霸。 笑死疆瑰,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的昙啄。 我是一名探鬼主播穆役,決...
    沈念sama閱讀 40,468評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼梳凛!你這毒婦竟也來(lái)了耿币?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤韧拒,失蹤者是張志新(化名)和其女友劉穎淹接,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體叭莫,經(jīng)...
    沈念sama閱讀 45,850評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蹈集,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評(píng)論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了雇初。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拢肆。...
    茶點(diǎn)故事閱讀 40,144評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖靖诗,靈堂內(nèi)的尸體忽然破棺而出郭怪,到底是詐尸還是另有隱情,我是刑警寧澤刊橘,帶...
    沈念sama閱讀 35,823評(píng)論 5 346
  • 正文 年R本政府宣布鄙才,位于F島的核電站,受9級(jí)特大地震影響促绵,放射性物質(zhì)發(fā)生泄漏攒庵。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評(píng)論 3 331
  • 文/蒙蒙 一败晴、第九天 我趴在偏房一處隱蔽的房頂上張望浓冒。 院中可真熱鬧,春花似錦尖坤、人聲如沸稳懒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)场梆。三九已至墅冷,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間或油,已是汗流浹背寞忿。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留装哆,地道東北人罐脊。 一個(gè)月前我還...
    沈念sama閱讀 48,415評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像蜕琴,于是被迫代替她去往敵國(guó)和親萍桌。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評(píng)論 2 355

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