问题背景
前段时间碰到客户问题发现是 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());}
}
执行结果
查看直接结果会发现 循环添加的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相同的会导致数据丢失
在执行VisibilityDemo
的main
方法时,
-
执行第一个
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
执行时first
和i
的值
-
第二个
computeIfAbsent
执行时first
和i
的值
-
第一个
computeIfAbsent
开始赋值时,first和i的值,以及 tab[i]的值
。
解决方法
- 升级JDK版本,目前测试jdk17执行相同代码会提示
ConcurrentModificationException
异常,直接中止 - 禁止在 computeIfAbsent 方法中套用 该对象的computeIfAbsent方法。