【问题】ConcurrentHashMap死循环问题分析
问题背景
在处理客户问题时,有个方法一调用就卡住了,最后排查时发现堆栈一直卡在java.util.concurrent.ConcurrentHashMap#computeIfAbsent代码中,一直在进行循环,最后确认是在该代码中死循环了。
发生原因
在使用ConcurrentHashMap的computeIfAbsent方法是,如果循环赋值的两个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("结束");
}
}
执行以上代码控制台不会输出【结束】,因为后台已经死循环了,感兴趣的同学可以本地执行看看。
在ConcurrentHashMap的computeIfAbsent 方法中 有个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;
}
在执行VisibilityDemo的main方法时,
- 第一个
computeIfAbsent构造key为AaAa节点时,执行的时注释的 1 2 3 三步,第三步时开始执行 第二个computeIfAbsent方法 - 此时第一个
computeIfAbsent的f节点是一个node对象,但是keyvalue都是null - 第二个
computeIfAbsent中也会执行第2步,获取f节点,由于key的hash相同,所以此时获取到的是第一步初始化为完成的节点keyvalue都是null, - 执行到注释 5 的时候,需要判断hash值,hash大于0代表f节点正常可以向后追加节点,此时情况不满足要求,所以导致了死循环。
第一个 computeIfAbsent执行时,节点 r 地址是734,在 casTabAt 方法中放到了i=15的桶中 ,然后调用value的lambda表达式

第二个 computeIfAbsent执行时,由于hash相同,获取到的f节点是第一步构造的节点,对象地址也是734,导致hash小于0一直不能进行赋值

解决办法
- 升级JDK版本,目前测试jdk17执行相同代码会提示
IllegalStateException: Recursive update,直接中止 - 禁止在
computeIfAbsent方法中套用 该对象的computeIfAbsent方法。
Stack Overflow 链接
https://stackoverflow.com/questions/43861945/deadlock-in-concurrenthashmap
思考下 JDK1.8的
ConcurrentHashMap computeIfAbsent会导致死循环,那么HashMap computeIfAbsent会有异常吗?
答案是肯定的,会丢失数据,感兴趣得话可以查看HashMap的computeIfAbsent方法丢失数据问题分析

浙公网安备 33010602011771号