【问题】HashMap的computeIfAbsent方法丢失数据问题分析

问题背景

前段时间碰到客户问题发现是 ConcurrentHashMap的computeIfAbsent导致死循环(ConcurrentHashMap死循环问题分析)就很好奇HashMap的computeIfAbsent会不会也有问题,一试之下发现确实存在问题,相同的代码在HashMap中会丢失插入的数据。

发生原因

【循环添加】时,如果key的hash相同,会导致前面的值得覆盖,而不是追加,所以导致数据丢失

源码分析

可复现代码

public class VisibilityDemo {


    public static void main(String[] args)  {

        Map<String,String> map = new HashMap<>();
        //循环添加
        map.computeIfAbsent("AaAa",v->map.computeIfAbsent("BBBB",v1->"a"));
        System.out.println("value:"+map+"size:"+map.size());
        Map<String,String> map1 = new HashMap<>();
        //分开添加
        map1.computeIfAbsent("AaAa",v->"a");
        map1.computeIfAbsent("BBBB",v1->"a");
        System.out.println("value:"+map1+"size:"+map1.size());
    }
}

执行结果

image

查看直接结果会发现 循环添加的map打印出来的value是一个,但是size=2,证明数据添加是执行了,但是数据丢失了

JDK源码 (1.8)

    public V computeIfAbsent(K key,
                             Function<? super K, ? extends V> mappingFunction) {
        if (mappingFunction == null)
            throw new NullPointerException();
        int hash = hash(key);
        Node<K,V>[] tab; Node<K,V> first; int n, i;
        int binCount = 0;
        TreeNode<K,V> t = null;
        Node<K,V> old = null;
        //1、如果table为null,进行初始化
        if (size > threshold || (tab = table) == null ||
            (n = tab.length) == 0)
            n = (tab = resize()).length;
        //2、第一个节点不为null,进行赋值
        if ((first = tab[i = (n - 1) & hash]) != null) {
            if (first instanceof TreeNode)
                old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
            else {
                Node<K,V> e = first; K k;
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k)))) {
                        old = e;
                        break;
                    }
                    ++binCount;
                } while ((e = e.next) != null);
            }
            V oldValue;
            if (old != null && (oldValue = old.value) != null) {
                afterNodeAccess(old);
                return oldValue;
            }
        }
        //3、执行 value的lambda表达式
        V v = mappingFunction.apply(key);
        if (v == null) {
            return null;
        } else if (old != null) {
            old.value = v;
            afterNodeAccess(old);
            return v;
        }
        else if (t != null)
            t.putTreeVal(this, tab, hash, key, v);
        else {
            //4、给 tab 的第i个桶进行赋值
            tab[i] = newNode(hash, key, v, first);
            if (binCount >= TREEIFY_THRESHOLD - 1)
                treeifyBin(tab, hash);
        }
        ++modCount;
        ++size;
        afterNodeInsertion(true);
        return v;
    }

通过查看【循环添加】和【分开添加】map打印出来的内容可以看出,在HashMap中循环添加hash相同的会导致数据丢失
在执行VisibilityDemomain方法时,

  • 执行第一个computeIfAbsent,执行注释中的 1 2 3 个步骤,步骤3时开始执行第二个 computeIfAbsent方法;

  • 此时代码逻辑认为是在构造first节点,所以此时first=null;

  • 第二个computeIfAbsent方法执行2 3 4,给tab对象的第 i个桶设置first节点;

  • 此时 i=15,所以是给tab的第15个桶进行赋值,然后返回,继续执行第一个 computeIfAbsent;

  • 第一个computeIfAbsent还会执行一次步骤4,因为hash相同所以 同样是给 i=15的桶赋值,导致第二个computeIfAbsent的值丢失了

  • 第一个computeIfAbsent执行时 firsti的值
    image

  • 第二个computeIfAbsent执行时 firsti的值
    image

  • 第一个computeIfAbsent开始赋值时,first和i的值,以及 tab[i]的值
    image

解决方法

  1. 升级JDK版本,目前测试jdk17执行相同代码会提示 ConcurrentModificationException异常,直接中止
  2. 禁止在 computeIfAbsent 方法中套用 该对象的computeIfAbsent方法。
posted @ 2025-03-09 23:55  此木|西贝  阅读(119)  评论(0)    收藏  举报