JDK8 ConcurrentHashMap computeIfAbsent bug分析

前段时间准备研究一波Mybatis,代码下载到IDEA,一路Debug走了一遍,头已经绕晕,准备放弃突然看到一些英文
JDK8 ConcurrentHashMap computeIfAbsent bug分析_第1张图片
上面写了啥bug 啥的,于是打开链接看了下,大概就是说的computeIfAbsent 方法,如果key存在的情况下也会加锁,会影响性能,后面又百度了下,发现还有其他bug

在写本文前,也看了些网上的文章,大概就是说的是在调用computeIfAbsent(key, …)方法时,正好其他线程需要在key对应位置插入结点,因为computeIfAbsent方法将位置设置了ReservationNode,因此putVal方法中那几个判断条件都不会满足,而导致死循环,只要computeIfAbsent结束了,ReservationNode节点都会被替换,此时循环也就结束了

终归揭底都是computeIfAbsent方法的问题,现在听我一一道来

本文只介绍ComputeIfAbsent核心内容,其他具体知识请阅读相应的文章
推荐一篇ConcurrentHashMap文章,个人感觉写得还不错:https://juejin.cn/post/6956468382736580622

computeIfAbsent

absent: 不存在, 意思就是如果不存在就计算

  • 如果hash(key) 映射到table数组 中的槽是空, 说明key 不存在, 那么根据key计算一个value,插入该元素
    • 根据函数得出value,最终插入Node (key, value) 到该槽中
  • 如果hash(key) 映射到table数组 中的槽不为空,那么对槽中的Node节点加锁进行遍历,判断是否有对应的key存在
    • 如果key对应的Node存在, 那么直接返回value
    • key对应的Node不存在,使用函数对key计算得出一个val, 如果val不为null,那么直接新将一个Node(key, val) 添加到链表尾部或则添加到红黑树, 返回val

该方法在JDK8 中有bug

public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
    if (key == null || mappingFunction == null)
        throw new NullPointerException();
    // 计算hash值
    int h = spread(key.hashCode());
    V val = null;
    // 记录链表中的元素数量,如果大于8 就进行树化,转为红黑树
    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();
        // 数组中i位置是空的
        else if ((f = tabAt(tab, i = (n - 1) & h)) == null) {
            // 创建一个站位的节点
            Node<K,V> r = new ReservationNode<K,V>();
            synchronized (r) {
				// 将i位置设置为占位节点
                if (casTabAt(tab, i, null, r)) {
                    binCount = 1;
                    Node<K,V> node = null;
                    try {
                        // 计算key对应的value
                        if ((val = mappingFunction.apply(key)) != null)
                            // 根据得出的value创建一个节点
                            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 {
            boolean added = false;
            // 对节点进行加锁,防止其他线程操作
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    // i位置是一个链表,那么遍历这个链表寻找是否有key对应的节点
                    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;
                            // 找到了链表最后,没有发现跟key相同的节点,那么调用函数计算val
                            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;
}
computeIfPresent

Present: 存在

  • 如果hash(key) 映射到table数组 中的槽是空, 说明key 不存在, 直接返回null

  • 如果hash(key) 映射到table数组 中的槽不为空,那么对槽中的Node节点加锁进行遍历,判断是否有对应的key存在

    • 如果key在map中已经存在,那么调用remappingFunction重新计算一个新的value,计算出的vlalue不为null,那么将新的value替换旧value, 如果为计算结果为null, 那么将该节点删除,返回新的value
    • 如果key没有在map上,直接返回null
public V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
    // 判断是否为null
    if (key == null || remappingFunction == null)
        throw new NullPointerException();
    // 获取hash值
    int h = spread(key.hashCode());
    V val = null;
    int delta = 0;
    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();
        // 计算key所对应table中哪一个元素
        else if ((f = tabAt(tab, i = (n - 1) & h)) == null)
            break;
        // 是否在扩容
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            // 对key锁对应的元素table[i]加锁
            synchronized (f) {
                // 元素是否被转移
                if (tabAt(tab, i) == f) {
                    // fh >= 0: 说明此时是链表
                    if (fh >= 0) {
                        binCount = 1;
                        // 遍历table[i] 中的元素,寻找key相同的元素
                        for (Node<K,V> e = f, pred = null;; ++binCount) {
                            K ek;
                            // 成立:说明找到key相同的元素
                            if (e.hash == h &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                // 计算得出一个新的value
                                val = remappingFunction.apply(key, e.val);
                                // 不为null,直接赋值
                                if (val != null)
                                    e.val = val;
                                else {	// 为null,那么key对应的元素将被删除
                                    delta = -1;
                                    Node<K,V> en = e.next;
                                    if (pred != null)
                                        pred.next = en;
                                    else
                                        setTabAt(tab, i, en);
                                }
                                break;
                            }
                            pred = e;
                            if ((e = e.next) == 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 = remappingFunction.apply(key, p.val);
                            if (val != null)
                                p.val = val;
                            else {
                                delta = -1;
                                if (t.removeTreeNode(p))
                                    setTabAt(tab, i, untreeify(t.first));
                            }
                        }
                    }
                }
            }
            if (binCount != 0)
                break;
        }
    }
    // 当调用remappingFunction后计算得出为null时,delta会变为-1,意味着key对应的元素删除,这里将map数量-1
    if (delta != 0)
        addCount((long)delta, binCount);
    // 返回计算得出的值
    return val;
}
BUG分析

这里只分析computeIfAbsent代码,computeIfPresent类似

bug1

实例代码:

public static void main(String[] args) {
    Map<String, Integer> map = new ConcurrentHashMap<>(16);
    // 这里会阻塞,因为AaAa的hash跟BBBB相同, 在插入BBBB时会导致for无限循环
    map.computeIfAbsent(
        "AaAa",
        key -> {
            return map.computeIfAbsent(
                "BBBB",
                key2 -> 42);
        }
    );

此代码来自https://bugs.openjdk.java.net/browse/JDK-8062841

以上面案例来分析BUG: 标号代码见最下面,建议将代码复制到一边,在读下面的解释

  1. AaAa跟BBBB计算出的hash值都是一样的,因此他们会放在同一个位置

  2. 在插入AaAa时,在执行①时,发现数组中没有这个元素,那么创建一个ReservationNode

    (hash = -3)节点作为占位,对该节点加synchronized锁,将该节点放在需要插入元素的位置i上(③)

  3. 向下执行到③,调用apply计算value,调用上述代码第7行的 map.computeIfAbsent(“BBBB”,

    key2 -> 42);将这个结果作为AaAa的value

  4. 执行上述7行代码后再次进入computeIfAbsent方法,此时①不成立,因为在第二步插入了占位符

  5. 判断条件⑤(表示的是这个位置是否正在进行扩容), 不成立,进入条件⑥,在⑦获取锁,因为当前线程在第二步已经获取过锁,因为synchronized是可重入的,继续向下走

  6. 进入条件⑧发现不满足,因为ReservationNode的hash为-3,向下执行,发现条件⑨也不满足

  7. 进入下一次for循环,往复执行 for语句和 ⑥⑦⑧⑨,导致死循环

由于广大开发者发现了这个bug, 官方也建议过不要使用递归调用,为了让开发者感知这个错误因此在JDK 9中对代码进行了升级

在上面的案例中,因为程序会在⑥⑦⑧⑨来回往复执行,程序bug主要是在⑧⑨判断时都无法满足条件,因此陷入了死循环,JDK 9 在⑨下面新增加了一个条件,代码如下

else if (f instanceof ReservationNode)
    throw new IllegalStateException("Recursive update");

当程序检查出f是一个占位符节点直接抛出异常,以此来阻止死循环

bug2
  1. 上面的案例是基于待插入元素位置i处为空的情况,如果i位置已经有元素的话,可能会造成数据丢失的bug, 如下代码:

    // 保证初始化空间为16(传入16 会变成32) 才能满足插入的三个元素在长度16的数组中是相同的位置
    ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(10);
    
    map.put("A0", 1);	// 这里会首先放到数组15位置, 另外两个计算结果同样在15位置
    // map.put("AaAa", 1);
    // map.put("BBBB", 1);
    
    map.computeIfAbsent(
        "AaAa",
        key -> {
            return map.computeIfAbsent(
                "BBBB",
                key2 -> 42);
        }
    );
    
    Enumeration<String> keys = map.keys();
    while (keys.hasMoreElements()) {
        String s = keys.nextElement();
        System.out.println(s);
    }
    
    

    运行上述代码会发现BBBB丢失

    此时运行过程将会变为如下:

    1. 首先插入A0到15位置

    2. 执行computeIfAbsent,插入AaAa节点(同样在15位置),执行⑦⑧,遍历链表到末尾(pred 记录链表最后一个元素,即A0),执行到⑩继续调用apply计算value

    3. 在次调用 map.computeIfAbsent(“BBBB”…. , 将pred.next 赋值为BBBB,即此时A0.next = BBBB, 返回42,回到第一次插入AaAa时

    4. 因为第一步的pred记录的是最开始链表最后一个节点A0,现在在次执行pred.next = AaAa,

      此时A0并没有指向第二步插入的BBBB节点了,BBBB节点就被丢失

    这种情况虽然不会死循环,但是会造成数据的丢失,因此为了防止这种情况发生,在④内部新增了代码:

     if ((val = mappingFunction.apply(key)) != null) {
     	 if (pred.next != null)
    	    throw new IllegalStateException("Recursive update");
    

    如果发现pred.next != null, 说明已经被赋值了,如果在次对pred.next赋值,就会造成数据丢失

    如下图:

    JDK8 ConcurrentHashMap computeIfAbsent bug分析_第2张图片

// 建议将代码复制到一边,在读上面的解释
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
   
    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 表为空  对表进行初始化
        // ①
        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);
                    }
                }
            }
           
        }
        // ⑤
        else if ((fh = f.hash) == MOVED)
        // ⑥
        else {
            // ⑦
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    // ⑧
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek; V ev;
                            if (存在key) {
                               // 将老的value替换为新的value
                                break;
                            }
                            Node<K,V> pred = e;
                            // ⑩
                            if ((e = e.next) == null) {
                                if ((val = mappingFunction.apply(key)) != null) {
                                    pred.next = new Node<K,V>(h, key, val, null);
                                }
                                break;
                            }
                        }
                    }
                    // ⑨
                    else if (f instanceof TreeBin) {
                        .... // 跟链表逻辑类似
                    }
                }
            }

        }
    }

    return val;
}

本人才疏学浅,文中若有错误,还请指出!

你可能感兴趣的:(多线程)