【问题】ConcurrentHashMap死循环问题分析

问题背景

在处理客户问题时,有个方法一调用就卡住了,最后排查时发现堆栈一直卡在java.util.concurrent.ConcurrentHashMap#computeIfAbsent代码中,一直在进行循环,最后确认是在该代码中死循环了。

发生原因

在使用ConcurrentHashMapcomputeIfAbsent方法是,如果循环赋值的两个key hash是相同的,在第一次构建node对象时就会发生死循环

可复现问题的代码

public class VisibilityDemo {


    public static void main(String[] args)  {
        Map<String,String> map = new ConcurrentHashMap<>();
        //AaAa  和  BBBB 这两个字符串的hash是相同的
        map.computeIfAbsent("AaAa",v->map.computeIfAbsent("BBBB",v1->"a"));
        System.out.printf("结束");
    }
}

执行以上代码控制台不会输出【结束】,因为后台已经死循环了,感兴趣的同学可以本地执行看看。
ConcurrentHashMapcomputeIfAbsent 方法中 有个for循环,在这里打上断点,就可以进入断点验证是否一直在循环执行。

死循环逻辑

    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;
        //开始循环,除非执行到break,否则一直循环
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            //1、tab为null,进行初始化
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            //2、tab不为null,则获取第一个节点 f (first)节点,如果f==null,则进行初始化
            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 {
                            //3、执行v的lambda表达式
                            if ((val = mappingFunction.apply(key)) != null)
                                node = new Node<K,V>(h, key, val, null);
                        } finally {
                            setTabAt(tab, i, node);
                        }
                    }
                }
                if (binCount != 0)
                    break;
            }
            //4、判断节点是否MOVED节点
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                boolean added = false;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                       //5、fh 是4获取到的hash,如果hash大于0代表是正常节点,
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek; V ev;
                                if (e.hash == h &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    val = e.val;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    if ((val = mappingFunction.apply(key)) != null) {
                                        added = true;
                                        pred.next = new Node<K,V>(h, key, val, null);
                                    }
                                    break;
                                }
                            }
                        }
                        //判断节点是否树节点
                        else if (f instanceof TreeBin) {
                            binCount = 2;
                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            TreeNode<K,V> r, p;
                            if ((r = t.root) != null &&
                                (p = r.findTreeNode(h, key, null)) != null)
                                val = p.val;
                            else if ((val = mappingFunction.apply(key)) != null) {
                                added = true;
                                t.putTreeVal(h, key, val);
                            }
                        }
                    }
                }

                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (!added)
                        return val;
                    break;
                }
            }
        }
        if (val != null)
            addCount(1L, binCount);
        return val;
    }

在执行VisibilityDemomain方法时,

  • 第一个computeIfAbsent构造key为AaAa节点时,执行的时注释的 1 2 3 三步,第三步时开始执行 第二个computeIfAbsent方法
  • 此时第一个computeIfAbsent 的f节点是一个node对象,但是 key value都是null
  • 第二个computeIfAbsent中也会执行第2步,获取f节点,由于key的hash相同,所以此时获取到的是第一步初始化为完成的节点 key value都是null,
  • 执行到注释 5 的时候,需要判断hash值,hash大于0代表f节点正常可以向后追加节点,此时情况不满足要求,所以导致了死循环。

第一个 computeIfAbsent执行时,节点 r 地址是734,在 casTabAt 方法中放到了i=15的桶中 ,然后调用value的lambda表达式
image
第二个 computeIfAbsent执行时,由于hash相同,获取到的f节点是第一步构造的节点,对象地址也是734,导致hash小于0一直不能进行赋值
image

解决办法

  1. 升级JDK版本,目前测试jdk17执行相同代码会提示 IllegalStateException: Recursive update,直接中止
  2. 禁止在 computeIfAbsent 方法中套用 该对象的computeIfAbsent方法。

Stack Overflow 链接

https://stackoverflow.com/questions/43861945/deadlock-in-concurrenthashmap

思考下 JDK1.8的 ConcurrentHashMap computeIfAbsent 会导致死循环,那么 HashMap computeIfAbsent 会有异常吗?
答案是肯定的,会丢失数据,感兴趣得话可以查看HashMap的computeIfAbsent方法丢失数据问题分析

posted @ 2025-03-09 23:15  此木|西贝  阅读(110)  评论(0)    收藏  举报