0
点赞
收藏
分享

微信扫一扫

ConcurrentHashMap.computeIfAbsent死循环

树下的老石头 2021-09-28 阅读 54
demo
package java8;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @Description:
 */
public class ConcurrentHashMapTest {

    public static void main(String[] args) {
        Map map =new ConcurrentHashMap();

        map.computeIfAbsent("a",key -> {

            map.put("a","v2");
            return"v1";

        });

    }
}

这段代码执行以后"a"对应的value到底是多少呢?

答案是执行这行代码的线程cpu占用会到100%,而且程序不退出。查看线程堆栈出现这样的情况:

"main" #1 prio=5 os_prio=0 tid=0x00007fab90009800 nid=0x5140 runnable [0x00007fa
b96191000]
   java.lang.Thread.State: RUNNABLE
        at java.util.concurrent.ConcurrentHashMap.putVal(ConcurrentHashMap.java:
1069)
        at java.util.concurrent.ConcurrentHashMap.put(ConcurrentHashMap.java:100
6)
        at ConcurrentHashMapTest.lambda$main$0(ConcurrentHashMapTest.java:15)
        at ConcurrentHashMapTest$$Lambda$1/471910020.apply(Unknown Source)
        at java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHash
Map.java:1660)
        - locked <0x00000000f68d9820> (a java.util.concurrent.ConcurrentHashMap$
ReservationNode)
        at ConcurrentHashMapTest.main(ConcurrentHashMapTest.java:13)

   Locked ownable synchronizers:

可以看到put方法和computeIfAbsent方法同时卡在了一个ReservationNode对象上。查看ConcurrentHashMap的源码可以发现,这种情况在bucket没有初始化的时候会发生,简单来说computeIfAbsent会在bucket为null的时候初始化一个ReservationNode来占位(方便解决多线程问题),然后等待后面的计算结果出来,再替换当前的占位对象,而putVal会synchorized这个对象,并根据其hash值的正负来进行更新,遗憾的时ReservationNode的hash是RESERVED 为-3,在putVal中没有处理过这种情况,然后就一直for循环处理了。

问题分析到这里,原因已经很清楚了,当时我们认为,这可能是jdk的“bug”, 但看了下computeIfAbsent的注释:

    /**
     * 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 {@code 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.

我们发现,其实人家已经知道了这个问题,还特意注释说明了。。。我们还是too yong too simple啊。至此,ConcurrentHashMap死循环问题告一段落,还是要遵循编码规范,不要在mappingFunction中再对当前map进行更新操作.

最后,一起跟着computeIfAbsent源码来分下上述死循环代码的执行流程,限于篇幅,只分析下主要流程代码:

 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());
 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)
10             tab = initTable();
11         else if ((f = tabAt(tab, i = (n - 1) & h)) == null) {
12             Node<K,V> r = new ReservationNode<K,V>();
13             synchronized (r) {
14                 // 这里使用synchronized针对局部对象意义不大,主要是下面的cas操作保证并发问题
15                 if (casTabAt(tab, i, null, r)) {
16                     binCount = 1;
17                     Node<K,V> node = null;
18                     try {
19                         // 这里的value返回可能为null呦
20                         if ((val = mappingFunction.apply(key)) != null)
21                             node = new Node<K,V>(h, key, val, null);
22                     } finally {
23                         setTabAt(tab, i, node);
24                     }
25                 }
26             }
27             if (binCount != 0)
28                 break;
29         }
30         else if ((fh = f.hash) == MOVED)
31             tab = helpTransfer(tab, f);
32         else {
33             boolean added = false;
34             synchronized (f) {
35                 // 仅仅判断了node.hash >=0和node为TreeBin类型情况,未判断`ReservationNode`类型
36                 // 扩容时判断和此处类似
37                 if (tabAt(tab, i) == f) {
38                     if (fh >= 0) {
39                         binCount = 1;
40                         for (Node<K,V> e = f;; ++binCount) {
41                             K ek; V ev;
42                             if (e.hash == h &&
43                                 ((ek = e.key) == key ||
44                                  (ek != null && key.equals(ek)))) {
45                                 val = e.val;
46                                 break;
47                             }
48                             Node<K,V> pred = e;
49                             if ((e = e.next) == null) {
50                                 if ((val = mappingFunction.apply(key)) != null) {
51                                     added = true;
52                                     pred.next = new Node<K,V>(h, key, val, null);
53                                 }
54                                 break;
55                             }
56                         }
57                     }
58                     else if (f instanceof TreeBin) {
59                         binCount = 2;
60                         TreeBin<K,V> t = (TreeBin<K,V>)f;
61                         TreeNode<K,V> r, p;
62                         if ((r = t.root) != null &&
63                             (p = r.findTreeNode(h, key, null)) != null)
64                             val = p.val;
65                         else if ((val = mappingFunction.apply(key)) != null) {
66                             added = true;
67                             t.putTreeVal(h, key, val);
68                         }
69                     }
70                 }
71             }
72             if (binCount != 0) {
73                 if (binCount >= TREEIFY_THRESHOLD)
74                     treeifyBin(tab, i);
75                 if (!added)
76                     return val;
77                 break;
78             }
79         }
80     }
81     if (val != null)
82         // 计数统计&阈值判断+扩容操作
83         addCount(1L, binCount);
84     return val;
85 }

如果非要在计算新值的过程中修改map,可以换一种方法来实现computeIfAbsent的功能:

V value = map.get(k);

if (value == null) {

    V newValue = computeValue(k);  // 这里对computeValue(k)的重复调用不敏感

    value = map.putIfAbsent(k, newValue);

   if (value == null) {

        return newValue;

   }

   return value;

}
举报

相关推荐

0 条评论