先看这么一段代码:
Map map =newHashMap<>();
map.computeIfAbsent("a",key -> {
map.put("a","v2");
return"v1";
});
这段代码执行以后"a"对应的value到底是多少呢?
答案是执行这行代码的线程cpu占用会到100%,而且程序不退出。查看线程堆栈出现这样的情况:
"main" #1 prio=5 os_prio=31 tid=0x00007f804f002000 nid=0x1703 runnable [0x0000700000218000]
java.lang.Thread.State: RUNNABLE
at java.util.concurrent.ConcurrentHashMap.putVal(ConcurrentHashMap.java:1069)
at java.util.concurrent.ConcurrentHashMap.put(ConcurrentHashMap.java:1006)
at com.wangqun.HelloComputeIfAbsent.lambda$main$0(HelloComputeIfAbsent.java:14)
at com.wangqun.HelloComputeIfAbsent$$Lambda$1/796533847.apply(Unknown Source)
at java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1660)
- locked <0x000000076b448b10> (a java.util.concurrent.ConcurrentHashMap$ReservationNode)
at com.wangqun.HelloComputeIfAbsent.main(HelloComputeIfAbsent.java:13)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
可以看到put方法和computeIfAbsent方法同时卡在了一个ReservationNode对象上。查看ConcurrentHashMap的源码可以发现,这种情况在bucket没有初始化的时候会发生,简单来说computeIfAbsent会在bucket为null的时候初始化一个ReservationNode来占位,然后等待后面的计算结果出来,再替换当前的占位对象,而putVal会synchorized这个对象,并根据其hash值的正负来进行更新,遗憾的时ReservationNode的hash是-3,在putVal中没有处理过这种情况,然后就一直for循环处理了。
这其实是一种编程bug,computeIfAbsent在使用的时候,计算value的过程中一定不能出现对map的修改操作,否则如果修改的key和computeIfAbsent的key分到同一个桶,而且那个bucket没有被使用过,就会悲剧。
如果非要在计算新值的过程中修改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;
}
对于HashMap.computeIfAbsent,这么调用则没有这种问题出现。