感谢我的同事「汝止」的分享。
问题描述
双十一备战前期,发现线上一台服务器偶发性的线程 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 对象。