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

2019-07-20 17:06:44 +08:00
 hiddenzzh

大家可能都听过 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 实战指南》,同时欢迎关注笔者的微信公众号:朱小厮的博客。


4167 次点击
所在节点    程序员
10 条回复
misaka19000
2019-07-20 17:14:52 +08:00
👍
neuthself
2019-07-20 17:58:54 +08:00
厮大厉害
luckylo
2019-07-20 21:27:17 +08:00
关于 computeIfAbsent ,见过一个类似的。参考: https://mp.weixin.qq.com/s/V4-BSor9AzZZgLcLWrKCrQ
另外就好比 Vector
,单个操作线程安全,同时多个就不一定,这应该是都了解的。
cubecube
2019-07-20 22:03:59 +08:00
说话最好说完吧
momocraft
2019-07-20 23:09:31 +08:00
前面這種也叫遞歸? 我以爲是叫重入
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
2019-07-21 02:27:36 +08:00
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
2019-07-21 03:06:51 +08:00
恶心的推广贴,还是 copy 别人博客
bobuick
2019-07-21 07:29:57 +08:00
什么 jb 玩意, 就发的飞起. 搞的好像发现了个大新闻似的
Aresxue
2019-07-22 10:39:18 +08:00
哗众取宠。。。computeIfAbsent 里面实现的用到了占位节点并会锁住该节点,所以多层嵌套时会发生死锁。这个东西不了解也没啥问题,毕竟很多人可能都没怎么用过 ConcurrentHashMap,用的对不对还要打个问号。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/584703

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX