WhatAKitty Daily

A Programmer's Daily Record

ConcurrentHashMap RESERVED状态死循环

WhatAKitty   阅读次数loading...

背景

今天本来是想要优化下自己的OTA代码,使得效率更高,结果被现实赤裸裸打了脸。

过程

如下是原先的代码:

1
2
3
4
5
6
7
private static final Map<String, Object> holder = new ConcurrentHashMap<>(16);

if (holder.get(clazzName) == null) {
holder.putIfAbsent(clazzName, createSingleton(clazz, params));
}

return (T) holder.get(clazzName);

如上代码是笔者一个单例创建工具的一部分代码,今天在浏览Map接口的时候,发现了几个方法:

1
2
3
4
5
default V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction);

default V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction);

default V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction);

很好,通过这几个方法,可以完美的吧老代码替换为如下代码:

1
return (T) holder.computeIfAbsent(clazzName, key -> createSingleton(clazz, params));

一句话搞定,笔者美滋滋的在更改完毕后运行程序,然后发现了一个非常坑爹的现象:笔者的应用阻塞在某个点上,一致无法执行下去。

调试

一开始,笔者以为是自己的单例创建工具除了问题。

因为,这个单例工具参照了IOC的设计,将对象存储到容器内。如果有调用方通过类以及构造实参调用,就会判断容器内是否存在,如果存在则直接返回对象、不存在则通过类以及构造实参创建一个实例返回。

然后在单步调试的过程中,发现竟然卡在了holder.computeIfAbsent这个方法上,那就step into进去看看是什么原因导致的问题。

最终经过一系列调试,将问题定位在了如下的代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
else {
boolean added = false;
synchronized (f) {
if (tabAt(tab, i) == f) {
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;
}
}

这是ConcurrentHashMap,在获取key所对应的桶(ConcurrentHashMap内部一维数组桶+链表(红黑树)结构)的过程,如果桶不为空且不在扩容的过程中,则会执行上述这段代码。

这段代码肯定没啥问题,那为啥会导致一直在死循环,而没法进入某个具体的执行过程呢(fh一直小于0,且不是红黑树节点)?

仔细看了下后,笔者意外的发现,所获取到的节点竟然是一个暂存态节点。什么叫暂存态,即这个节点没有真正初始化完毕,节点的状态为RESERVED(-3)。

定位到原因:容器内存在正在初始化的节点,好巧不巧的是,另外有调用方使用computeIfAbsent方法,传入的keyhash后的节点正好是初始化的节点,那就悲剧了。后面的方法需要等待这个节点初始化完毕后才会执行成功,否则将永远处于while死循环的状态。而笔者的这种情况就是最坏的情况,不知道什么原因导致前者初始化一直完成不了。

解决

好了,定位到了问题所在,现在的关注点在于,为什么会出现前者一致处于初始化的情况?

笔者在获取单例的方法上做了断点,每次调用后,都查看下容器内的table属性是否存在初始化的节点。然后发现了某个对象(A)在获取单例后,没有初始化完成,而又在构造方法内进行了单例类(B)的获取。好巧不巧,A和B的keyhash后的桶索引是一样的,也就造成了A对象在等初始化完成,初始化内的B对象在初始化过程中,由于hash冲突,等待节点的初始化完成,完美的形成了死循环。

疑问点

问题以及原因虽然找到了,但是为啥之前的写法会没有问题呢?之前也是使用了ConcurrentHashMap作为对象存储的容器,按理也会出现这个问题。仔细看了看代码后,笔者恍然,之前的问题所在主要是computeIfAbsent在调用过程中被阻塞(调用过程中做了对象的创建)。而老的代码是:在创建成功单例类之前,是不存在map.put的过程,自然也不会出现前者的情况。