今天,感谢我的同事「汝止」的分享。

双十一前期,发现线上一台服务器偶发性的线程 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 对象上。ReservationNode 在 computeIfAbsent() 方法构建 value 值的时候被用作占位节点。

参见源码: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;    }

发生死循环是由于在初始化 bucket 的时候,computeIfAbsent() 方法初始化一个 ReservationNode 来占位。等待计算完毕后替换当前的占位对象。此时,又触发了 ConcurrentHashMap 扩容。

参见源码:java.util.concurrent.ConcurrentHashMap#transfer

ConcurrentHashMap 扩容忽略了 ReservationNode 情况,因此导致死锁,然后就一直 for 循环处理了。

值得庆幸的是,JDK 1.9 解决了这个问题,但是对于 JDK 1.8 的用户还是需要规避的。


更多相关文章

  1. java任务调度(1)Timer定时器(案例和源码分析)
  2. java集合系列(10)Hashtable源码分析(jdk1.8,推荐)
  3. java集合系列(8)HashMap(源码分析,强烈推荐!!!)
  4. Java集合LinkedList源码剖析
  5. Java集合HashSet源码剖析
  6. Java 死锁范例以及如何分析死锁
  7. 不瞒你说,我最近跟Java源码杠上了
  8. [原]jQuery Tab插件,用于在Tab中显示iframe,附源码和详细说明

随机推荐

  1. Android定时任务的实现
  2. Android LinearLayout中实现水平方向控件
  3. Android——SharedPreferences
  4. android 常用布局有哪些
  5. Android工程导入时常见的错误解决方法
  6. Android ellipsize属性(多余文字用省略号
  7. android限制文本长度
  8. android ListView 示例1 entries 指定一
  9. Android:解决ListView按下后上下滑动背景
  10. Android studio was unable to find a va