V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
hiddenzzh
V2EX  ›  程序员

不止 JDK7 的 HashMap, JDK8 的 ConcurrentHashMap 也会造成 CPU 100%

  •  1
     
  •   hiddenzzh · 2019-07-20 17:06:44 +08:00 · 3933 次点击
    这是一个创建于 1713 天前的主题,其中的信息可能已经有所发展或是发生改变。

    大家可能都听过 JDK7 中的 HashMap 在多线程环境下可能造成 CPU 100%的现象,这个由于在扩容的时候 put 时产生了死链,由此会在 get 时造成了 CPU 100%。这个问题在 JDK8 中的 HashMap 获得了解决。其实 JDK7 中的 HashMap 在多线程环境下不止只有 CPU 100%这一共怪异现象,它还可能造成插入的数据丢失,有兴趣的读者可以自行了解下。

    对于 HashMap 多线程的问题,我们通常会这么反问:HashMap 设计上就不是多线程安全的,何必要去在多线程环境下用呢?的确如此,我们不会傻到显式的在多线程环境下调用,但是又可能在你所关注的视角范围外是多线程的,其隐式地让 HashMap 置于多线程环境下了,这个又难以一下子察觉到。再者,对于 HashMap 多线程的问题,我们很多时候推荐使用 ConcurrentHashMap 来代替 HashMap 应用于多线程的环境,很不巧的是 ConcurrentHashMap 也有可能会造成 CPU 100%的异常现象。这个怪异现象存在于 JDK8 的 ConcurrentHashMap 中,在 JDK9 中已经得到修复,可以参见: https://bugs.openjdk.java.net/browse/JDK-8062841

    什么情况下 JDK8 的 ConcurrentHashMap 会出现这个 Bug 呢?首先我们来运行一下这段代码:

    Map<String, String> map = new ConcurrentHashMap<>();
    map.computeIfAbsent("AaAa",
            key -> map.computeIfAbsent("BBBB", key2 -> "value"));
    

    你会惊奇的发现这个程序一直处于 Running 状态,我们通过 top -Hp [pid]命令查看到其中一个线程的 CPU 使用率接近 100%,参考下图:

    在这里插入图片描述

    可以看到 pid 为 31417 的东东,我们再通过 jstack -l [pid]命令查看到对应的线程为:

    在这里插入图片描述

    注意将 nid=0x7ab9 的 16 进制转为 10 进制就是 31417。可以看到问题是发生在了 computeIfAbsent 方法中,我们将示例中的程序换成下面这段程序也会同样出现 CPU 100%的 Bug:

    map.computeIfAbsent("AaAa",
            (String key) -> {
                map.put("BBBB", "value");
                return "value";
            });
    

    问题的关键在于递归使用了 computeIfAbsent 方法,笔者在 stackoverflow 上还搜索到了同类型的问题,下面的示例程序中调用 fibonacci 方法同样也会造成 CPU 100%.

    static Map<Integer, Integer> concurrentMap = new ConcurrentHashMap<>();
    
    public static void main(String[] args) {
        System.out.println("Fibonacci result for 20 is" + fibonacci(20));
    }
    
    static int fibonacci(int i) {
        if (i == 0)
            return i;
    
        if (i == 1)
            return 1;
    
        return concurrentMap.computeIfAbsent(i, (key) -> {
            System.out.println("Value is " + key);
            return fibonacci(i - 2) + fibonacci(i - 1);
        });
    }
    

    至于为什么会发生这个 BUG,答案就在 ConcurrentHashMap 中的 computeIfAbsent 方法中,自己去捞吧,嘿嘿。或者等我下一篇 怎么规避这个问题呢?只要不在递归中使用 computeIfAbsent 方法就好啦,或者降级用可爱的分段锁,或者升级 JDK9~


    欢迎支持笔者新作:《深入理解 Kafka:核心设计与实践原理》和《 RabbitMQ 实战指南》,同时欢迎关注笔者的微信公众号:朱小厮的博客。


    10 条回复    2019-07-22 10:39:18 +08:00
    misaka19000
        1
    misaka19000  
       2019-07-20 17:14:52 +08:00 via Android
    👍
    neuthself
        2
    neuthself  
       2019-07-20 17:58:54 +08:00   ❤️ 1
    厮大厉害
    luckylo
        3
    luckylo  
       2019-07-20 21:27:17 +08:00 via Android
    关于 computeIfAbsent ,见过一个类似的。参考: https://mp.weixin.qq.com/s/V4-BSor9AzZZgLcLWrKCrQ
    另外就好比 Vector
    ,单个操作线程安全,同时多个就不一定,这应该是都了解的。
    cubecube
        4
    cubecube  
       2019-07-20 22:03:59 +08:00 via Android
    说话最好说完吧
    momocraft
        5
    momocraft  
       2019-07-20 23:09:31 +08:00
    前面這種也叫遞歸? 我以爲是叫重入
    micean
        6
    micean  
       2019-07-20 23:29:26 +08:00
    computeIfAbsent 的第二个参数执行的时候处于第一个参数的桶被锁住的时候,所以安全的用法是别调用自身写函数

    map.computeIfAbsent("1", key -> map.put("2","value")); // 不会被锁住
    map.computeIfAbsent("2", key -> map.put("2","value")); // 死锁
    laminux29
        7
    laminux29  
       2019-07-21 02:27:36 +08:00   ❤️ 5
    1.有时候,多看看文档,很多误会,就会止于智者。

    URL:
    https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ConcurrentHashMap.html#computeIfAbsent-K-java.util.function.Function-

    If the specified key is not already associated with a value, attempts to compute its value using the given mapping function and enters it into this map unless null. The entire method invocation is performed atomically, so the function is applied at most once per key. Some attempted update operations on this map by other threads may be blocked while computation is in progress, so the computation should be short and simple, and must not attempt to update any other mappings of this map.

    简单翻译一下,就是 ConcurrentHashMap->computeIfAbsent 里的方法,不允许修改别的 key。


    2.这事情的原理,在计算机本科的数据库课程,或操作系统课程中,关于多线程与死锁问题的章节中,会有很详细的描述。题主应该加强知识的复习与巩固,在一个博士都多如牛毛的时代,对本科中很基本的知识掌握不牢,会影响自身的基础竞争力。
    nexusone
        8
    nexusone  
       2019-07-21 03:06:51 +08:00
    恶心的推广贴,还是 copy 别人博客
    bobuick
        9
    bobuick  
       2019-07-21 07:29:57 +08:00
    什么 jb 玩意, 就发的飞起. 搞的好像发现了个大新闻似的
    Aresxue
        10
    Aresxue  
       2019-07-22 10:39:18 +08:00
    哗众取宠。。。computeIfAbsent 里面实现的用到了占位节点并会锁住该节点,所以多层嵌套时会发生死锁。这个东西不了解也没啥问题,毕竟很多人可能都没怎么用过 ConcurrentHashMap,用的对不对还要打个问号。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   982 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 28ms · UTC 21:59 · PVG 05:59 · LAX 14:59 · JFK 17:59
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.