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;
}