ConcurrentHashMap的一个bug

最近发现java 1.8的concurrentHashMap,在使用computeIfAbsent时,如果涉及修改map,则会产生bug。
示例代码如下:

        System.out.println("start.");
        map.computeIfAbsent("t",
                (String t) -> map.computeIfAbsent("t", (String i) -> "i")); //halt在这里
        System.out.println("fin.");

如果执行这段代码,你会发现代码会停在注释出,一直没有结果。
最开始以为是递归实现的问题,通俗的说,就是在构造一个函数的时候陷入了自递归。就是你想构造一个A,但是A的构造依赖A已完成构造后的某些属性。为了验证是否是这个原因,我们把代码做一些调整,消除递归调用。

        ConcurrentHashMap map = new ConcurrentHashMap<>();
        System.out.println("start.");
        map.computeIfAbsent("t",
                (String t) -> {
                    map.put("t", "t");
                    return "t";
                });
        System.out.println("fin.");

你会发现,代码继续停在哪里,无法输出"fin."。

然后怀疑是死锁,怀疑concurrentHashMap使用了非可重入锁。但是跟着看conrrentHashMap的实现,发现是基于cas + synchronized的方式实现,而synchronized本身是可重入的,因此这里不满足死锁的条件。
继续看concurrentHashMap的注释,里面有这样一句话:

/*
  must not attempt to update any other mappings of this map.
*/

这句话确定了这个问题应该是已知存在的。
所以应该绝对避免在computeIfAbsent中有递归,或者修改map的任何操作

为了搞清楚原因,我们继续debug concurrentHashMap的源码,发现这种在computeIfAbsent中,如果尝试修改map的情况下,代码会在

        for (Node[] tab = table;;) {  //无限循环
            Node f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & h)) == null) {
                Node r = new ReservationNode();
                synchronized (r) {
                    if (casTabAt(tab, i, null, r)) { //cas
....

中反复循环。
我尝试通俗的解释一下这个问题:
注:不见得正确,只是个人理解
由于concurrentHashMap中使用的是cas操作,因此在出现cas嵌套的情况下,就会形成一种『死锁』。举例来说,一个值原来是 1, 我想把它修改成2,正常的cas操作,会比较在修改的那一刻,值是否仍然为1。这种比较,在cas只有一层的情况下,是没有问题的。但是,假如有两层cas,这个值原来是1,第一层把 1 -> 2,在cas还没有生效时,继续进入第二层cas操作,把 2 -> 3,当最终提交时,第二层cas比较当前值是否是2,但由于当前指仍然是1,因此修改无效。最终反复进入循环,形成死锁。

虽然computeIfAbsent的代码注释中对这种修改map的行为做了强提示,但在实际中,我认为这种行为仍旧是concurrentHashMap的一个实现bug。

https://bugs.openjdk.java.net/browse/JDK-8172951
好在这个问题在java 1.9中已经基本修复了。

This is fixed in JDK 9 with JDK-8071667 . When the test case is run in JDK 9-ea, it gives a ConcurrentModification Exception.
java 9

你可能感兴趣的:(ConcurrentHashMap的一个bug)