感谢我的同事「汝止」的分享。
问题描述
双十一备战前期,发现线上一台服务器偶发性的线程 cpu 占用会到 100%,并且一直饱和,需要重启服务器才能恢复。进过排查发现 ConcurrentHashMap.computeIfAbsent 在 JDK 1.8 存在 BUG。
原因分析
现在,我们从真实的业务代码中,模拟一个类似的场景。
public class ConcurrentHashMapDemo {
private Map<Integer, Integer> cache = new ConcurrentHashMap<>(15);
public static void main(String[] args) {
ConcurrentHashMapDemo ch = new ConcurrentHashMapDemo();
System.out.println(ch.fibonaacci(80));
}
public int fibonaacci(Integer i) {
if (i == 0 || i == 1) {
return i;
}
return cache.computeIfAbsent(i, (key) -> {
System.out.println("fibonaacci : " + key);
return fibonaacci(key - 1) + fibonaacci(key - 2);
});
}
}
如果你将这个代码跑起来,你会发现的这个程序将进入死循环,而无法结束。通过阅读源码发现,ConcurrentHashMap.computeIfAbsent() 方法停留在了一个 ReservationNode 对象上。
参见源码:
java.util.concurrent.ConcurrentHashMap#computeIfAbsent
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
if (key == null || mappingFunction == null)
throw new NullPointerException();
int h = spread(key.hashCode());
V val = null;
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> 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<K,V> r = new ReservationNode<K,V>();
synchronized (r) {
if (casTabAt(tab, i, null, r)) {
binCount = 1;
Node<K,V> node = null;
try {
if ((val = mappingFunction.apply(key)) != null)
node = new Node<K,V>(h, key, val, null);
} finally {
setTabAt(tab, i, node);
}
}
}
if (binCount != 0)
break;
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
// 忽略
}
}
if (val != null)
addCount(1L, binCount);
return val;
}
ReservationNode 在 computeIfAbsent() 方法构建 value 值的时候被用作占位节点。换句话说,computeIfAbsent() 方法会初始化一个 ReservationNode 来占位,它会等待计算完毕后替换当前的占位对象。此时,如果正好赶在 ConcurrentHashMap 达到容量 0.75 的时候进行扩容,由于ConcurrentHashMap 扩容忽略了 ReservationNode 情况。因此,可能会导致扩容无法替换占位符,同时占位符等待替换的情况,然后就一直 for 循环处理了。读者如果感兴趣,可以阅读扩容的源码:java.util.concurrent.ConcurrentHashMap#transfer
值得庆幸的是,JDK 1.9 解决了这个问题,但是对于 JDK 1.8 的用户还是需要规避的。
解决方案
发现问题就好办了,解决方案也很简单 : 不用递归的方式创建 ConcurrentHashMap 对象。