ConcurrentHashMap的computeIfAbsent方法在jdk8的bug

Posted gaokunlong

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ConcurrentHashMap的computeIfAbsent方法在jdk8的bug相关的知识,希望对你有一定的参考价值。

刚刚在头条看见一个说CHM(ConcurrentHashMap)在jdk8中的bug,自己亲自试了一下确实存在,并按照头条帖里面说的看了一下源码,记录一下

CHM的computeIfAbsent的方法是jdk8中新加的方法,也应用了jdk8的新特性,函数接口,lambda表达式;

方法说明:

 

public V computeIfAbsent(K key,
                         Function<? super K,? extends V> mappingFunction)
如果指定的键尚未与值相关联,则尝试使用给定的映射函数计算其值,并将其输入到此映射中,除非null 。 整个方法调用是以原子方式执行的,因此每个键最多应用一次该函数。 在计算过程中可能会阻止其他线程对该映射进行的某些尝试更新操作,因此计算应该简单而简单,不得尝试更新此映射的任何其他映射。
Specified by:
computeIfAbsent在界面 ConcurrentMap<K,V>
Specified by:
computeIfAbsent在界面 Map<K,V>
参数
key - 指定值与之关联的键
mappingFunction - 计算值的函数
结果
与指定键相关联的当前(现有或计算)值,如果计算值为空,则为null 

bug说明:

说明,jdk8:conmputeIfAbsent方法,获取key1的value,当值不存在是用mappingFunction返回的值设置到key1的value中
如果mappingFunction的返回逻辑也是用当前map的computeIfAbsent方法返回另外一个key2的值,而恰巧这两个key,key1和key2的hashCode值一样,即对应同一个table的槽,还有死循环的可能

正常的用法:
/**
 * 测试concurrentHashMapd的computIfAbsent方法bug
 * 说明,jdk8:conmputeIfAbsent方法,获取key1的value,当值不存在是用mappingFunction返回的值设置到key1的value中
 * 如果mappingFunction的返回逻辑也是用当前map的computeIfAbsent方法返回另外一个key2的值,而恰巧这两个key,key1和key2的hashCode值一样,即对应同一个table的槽,还有死循环的可能
 */
public class CopmuteIfAbsentTest {

    public static void main(String[] args) {
        System.out.println("AaAa".hashCode());
        System.out.println("BBBB".hashCode());

        //正常用法
        Map<String,String> map2 = new ConcurrentHashMap<>();
        String value1 = map2.computeIfAbsent("AaAa",n->"123");
        System.out.println(value1);
        //bug重现
        /*map2.computeIfAbsent("AaAa",(n)->{
            return map2.computeIfAbsent("BBBB",m->"123");
        });*/
    }
}

输出结果:

2031744
2031744
123

Process finished with exit code 0

 

将上面代码正常用法注释掉,放开bug重现部分,执行结果如果下:

2031744
2031744

 

代码一直没有退出,死循环

方法源 1 public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)  2 if (key == null || mappingFunction == null)

 3             throw new NullPointerException();
 4         int h = spread(key.hashCode()); //"AaAa"和"BBBB"的hash值相同,定位到tab的相同位置
 5         V val = null;
 6         int binCount = 0;
 7         for (Node<K,V>[] tab = table;;) { //此处死循环出不来
 8             Node<K,V> f; int n, i, fh;
 9             if (tab == null || (n = tab.length) == 0) //①测试代码第一次,ComputeIfAbsent("AaAa",...) 进入死循环后第一次走这tab初始化,第二个computeIfAbsent("BBBB",)不走这
10                 tab = initTable();
11             else if ((f = tabAt(tab, i = (n - 1) & h)) == null) {② //AaAa的computeIfAbsent方法发现此处table为空,创建占位Node,放进去,第二个computeIfAbsent("BBBB",)不走这
12                 Node<K,V> r = new ReservationNode<K,V>();
13                 synchronized (r) {
14                     if (casTabAt(tab, i, null, r)) {
15                         binCount = 1;
16                         Node<K,V> node = null;
17                         try {
18                             if ((val = mappingFunction.apply(key)) != null) //③在此处调用lambda表达式,进行计算等待返回值
19                                 node = new Node<K,V>(h, key, val, null);
20                         } finally {
21                             setTabAt(tab, i, node); //将计算结果创建node,顶替掉占位node
22                         }
23                     }
24                 }
25                 if (binCount != 0)
26                     break;
27             }
28             else if ((fh = f.hash) == MOVED) //③判断是否正在扩容,第二次computeIfAbsent时,f为占位node,hash为-3,不满足
29                 tab = helpTransfer(tab, f);
30             else { //④
31                 boolean added = false;
32                 synchronized (f) {
33                     if (tabAt(tab, i) == f) {
34                         if (fh >= 0) { //⑤判断是否是链表,第二次computeIfAbsent时,不满足
35                             binCount = 1;
36                             for (Node<K,V> e = f;; ++binCount) {
37                                 K ek; V ev;
38                                 if (e.hash == h &&
39                                     ((ek = e.key) == key ||
40                                      (ek != null && key.equals(ek)))) {
41                                     val = e.val;
42                                     break;
43                                 }
44                                 Node<K,V> pred = e;
45                                 if ((e = e.next) == null) {
46                                     if ((val = mappingFunction.apply(key)) != null) {
47                                         added = true;
48                                         pred.next = new Node<K,V>(h, key, val, null);
49                                     }
50                                     break;
51                                 }
52                             }
53                         }
54                         else if (f instanceof TreeBin) { //⑥判断是否是红黑树,第二次computeIfAbsent时不满足
55                             binCount = 2;
56                             TreeBin<K,V> t = (TreeBin<K,V>)f;
57                             TreeNode<K,V> r, p;
58                             if ((r = t.root) != null &&
59                                 (p = r.findTreeNode(h, key, null)) != null)
60                                 val = p.val;
61                             else if ((val = mappingFunction.apply(key)) != null) {
62                                 added = true;
63                                 t.putTreeVal(h, key, val);
64                             }
65                         }
66                     }
67                 }
68                 if (binCount != 0) { //⑦如果当前位置的元素个数大于0,返回
69                     if (binCount >= TREEIFY_THRESHOLD)
70                         treeifyBin(tab, i);
71                     if (!added)
72                         return val;
73                     break;
74                 }
75 } 76 } 77 if (val != null) 78 addCount(1L, binCount); 79 return val; 80 }

 

分析:

map2.computeIfAbsent("AaAa",。。。)会走①②③逻辑,此时在tab里放了一个占位node
map2.computeIfAbsent("BBB",。。。)会不满足①②,进入④,因为之前放了一个占位node,在里面⑤⑥都不满足,不进入其中逻辑,走⑦也不满足,循环中没有挑出的地方死循环。

解决:在jdk9中进行了解决,如果出现嵌套调用computeIfAbsent,并且正好key的hash值冲突,则在④里面的⑤⑥判断完是否是链表和红黑树之后增加判断是否是占位node,如果是就抛出异常
  • IllegalStateException - 如果计算可检测地尝试对此地图的递归更新,否则将永远不会完成 
 

 

以上是关于ConcurrentHashMap的computeIfAbsent方法在jdk8的bug的主要内容,如果未能解决你的问题,请参考以下文章

java多线程进阶ConcurrentHashMap

ConcurrentHashMap源码分析(1.8)

ConcurrentHashMap

JDK1.7中的ConcurrentHashMap

ConcurrentHashmap原理

ConcurrentHashmap原理