HashMap 1.8死锁问题详细分析

转载至: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也会出现死循环的问题(实测)

昨天在面试的时候,面试官问到HashMap在多线程并发的情况下怎么出现死循环的?
我当时非常有底气的回答:Hashmap在jdk1.8之前多线程同时进行put操作,并且同时进行扩容的时候可能会出现链表环,导致死循环的发生。
面试官紧接着问:那1.8就不会出现死循环了吗?
底气瞬间消失,思考一会:1.8在多线程的情况下HashMap会出现数据丢失的问题,比如…(给面试官举了个栗子)。
面试官毫无波澜,好像没有get到答案的样子问:那jdk1.8的HashMap是不是就不会出现死循环了?
我当时愣了一下,心想难道1.8也会出现死循环…考虑一会,最后决定坚持自己,但是已经没有了底气:是的。
面试官露出了开心的样子:嗯,好的。(看来肯定是说错了)


介绍完前因后果,那么来看一下Hashmap在jdk1.8会不会出现死循环的情况。

测试代码

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)]

发现cpu使用率99.9%,符合死循环的情况。
然后再使用 jps 和 jstack 命令查看一下进程的状态
[图片上传失败...(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;
                    //说明线程在这个for循环中一直没有返回,导致了死循环
                    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);
        }

这个方法看出,说明Hashmap在jdk1.8的时候也会出现死循环的情况,是在链表转换为树的时候for循环一直无法跳出,导致死循环。
不止这些,经过多次测试,发现死循环还会在2239行出现

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在多线程的情况下也会出现死循环的问题,但是1.8是在链表转换树或者对树进行操作的时候会出现线程安全的问题。

复习一下线程安全的定义:

在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。

HashMap在jdk1.8中也会死循环

jdk1.7版本中多线程同时对HashMap扩容时,会引起链表死循环,尽管jdk1.8修复了该问题,但是同样在jdk1.8版本中多线程操作hashMap时仍然会引起死循环,只是原因不一样。


示例代码

package com.gsonkeno.interview;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
/**
 *
 * jdk7扩容时都可能导致死锁
 * jdk8在PutTreeValue时可能死循环   死循环在hashMap的1816行或2229行, java version "1.8.0_111"
 * jstack发现可能卡在 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() + "执行结束完");
    }
}

代码见github

说明

[图片上传失败...(image-334c7d-1699429002669)]

多个线程全部全部在如下代码块的1816行,一直循环,无法退出for循环。

        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节点转换为TreeNode结点异常

主要原因其实是多线程下操作同一对象时,对象内部属性的不一致性导致的。分析方式跟为什么单例要使用双重锁check类似。

总结的经验就是,在多线程下不要使用HashMap,至少jdk8及其以下不要使用,之上版本也建议不要使用。

JDK8中HashMap依然会死循环

JDK8中HashMap依然会死循环!

是否你听说过JDK8之后HashMap已经解决的扩容死循环的问题,虽然HashMap依然说线程不安全,但是不会造成服务器load飙升的问题。

然而事实并非如此。少年可曾了解一种红黑树成环的场景,=v=

今日在查看监控时候发现,某一台机器load飙升
[图片上传失败...(image-1e4e75-1699429002669)]

感觉问题不对劲,ssh大法登陆机器,top,top -Hp,jstack,jmap四连击保存下来堆栈,cpu使用最高的线程,内存信息准备分析。

首先查看使用最耗费cpu的线程堆栈信息

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

[图片上传失败...(image-e21a45-1699429002669)]

我勒个去,HashMap,猜测八成死循环了,但是我们使用的JDK8,在8中通过栈封闭的链表替换,解决了扩容死循环的问题。疑惑,继续往下看。

根据堆栈信息,root方法是问题所在,点开HashMap源码

[图片上传失败...(image-6ae6eb-1699429002669)]

好嘛,load飙高,代码有个for语句,我觉得铁定死循环了,看代码情况只可能是两个红黑树节点的父亲节点相互引用才可以导致无法走出这个for语句。

然而这都是我的猜测,我没有证据。而且让我追红黑树的代码,也是需要耗费大量时间的事情,我需要快速验证我的猜测。

我之前dump下来了堆内存信息,我通过jhat 命令生成html的内存信息页面
[图片上传失败...(image-56d23d-1699429002668)]

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

我先找业务代码中持有这个HashMap的对象,然后点进去查询内部信息

[图片上传失败...(image-d12c47-1699429002668)]

因为数据都放在table中,点击Table字段,查看其内容

[图片上传失败...(image-604f65-1699429002668)]

table中存在唯一的一个TreeNode节点,这肯定是已经变成了红黑树了

点进去查看

[图片上传失败...(image-54a851-1699429002668)]

点击parent字段信息

[图片上传失败...(image-b6c931-1699429002668)]

0x72745d828与0x72745d7b8两个TreeNode节点的Parent引用都是对方。

后续打算深入研究一下红黑树什么场景会造成这个原因。

最后,无论什么并发场景请别使用HashMap,ConcurrentHashmap大法好

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,837评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,551评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,417评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,448评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,524评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,554评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,569评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,316评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,766评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,077评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,240评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,912评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,560评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,176评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,425评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,114评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,114评论 2 352

推荐阅读更多精彩内容