背景
今天本来是想要优化下自己的OTA代码,使得效率更高,结果被现实赤裸裸打了脸。
过程
如下是原先的代码:
1 | private static final Map<String, Object> holder = new ConcurrentHashMap<>(16); |
如上代码是笔者一个单例创建工具的一部分代码,今天在浏览Map
接口的时候,发现了几个方法:
1 | default V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction); |
很好,通过这几个方法,可以完美的吧老代码替换为如下代码:
1 | return (T) holder.computeIfAbsent(clazzName, key -> createSingleton(clazz, params)); |
一句话搞定,笔者美滋滋的在更改完毕后运行程序,然后发现了一个非常坑爹的现象:笔者的应用阻塞在某个点上,一致无法执行下去。
调试
一开始,笔者以为是自己的单例创建工具除了问题。
因为,这个单例工具参照了IOC的设计,将对象存储到容器内。如果有调用方通过类以及构造实参调用,就会判断容器内是否存在,如果存在则直接返回对象、不存在则通过类以及构造实参创建一个实例返回。
然后在单步调试的过程中,发现竟然卡在了holder.computeIfAbsent
这个方法上,那就step into
进去看看是什么原因导致的问题。
最终经过一系列调试,将问题定位在了如下的代码片段:
1 | else { |
这是ConcurrentHashMap
,在获取key
所对应的桶(ConcurrentHashMap
内部一维数组桶+链表(红黑树)结构)的过程,如果桶不为空且不在扩容的过程中,则会执行上述这段代码。
这段代码肯定没啥问题,那为啥会导致一直在死循环,而没法进入某个具体的执行过程呢(fh一直小于0,且不是红黑树节点)?
仔细看了下后,笔者意外的发现,所获取到的节点竟然是一个暂存态节点。什么叫暂存态,即这个节点没有真正初始化完毕,节点的状态为RESERVED
(-3)。
定位到原因:容器内存在正在初始化的节点,好巧不巧的是,另外有调用方使用computeIfAbsent
方法,传入的key
hash后的节点正好是初始化的节点,那就悲剧了。后面的方法需要等待这个节点初始化完毕后才会执行成功,否则将永远处于while
死循环的状态。而笔者的这种情况就是最坏的情况,不知道什么原因导致前者初始化一直完成不了。
解决
好了,定位到了问题所在,现在的关注点在于,为什么会出现前者一致处于初始化的情况?
笔者在获取单例的方法上做了断点,每次调用后,都查看下容器内的table
属性是否存在初始化的节点。然后发现了某个对象(A)在获取单例后,没有初始化完成,而又在构造方法内进行了单例类(B)的获取。好巧不巧,A和B的key
在hash
后的桶索引是一样的,也就造成了A对象在等初始化完成,初始化内的B对象在初始化过程中,由于hash
冲突,等待节点的初始化完成,完美的形成了死循环。
疑问点
问题以及原因虽然找到了,但是为啥之前的写法会没有问题呢?之前也是使用了ConcurrentHashMap
作为对象存储的容器,按理也会出现这个问题。仔细看了看代码后,笔者恍然,之前的问题所在主要是computeIfAbsent
在调用过程中被阻塞(调用过程中做了对象的创建)。而老的代码是:在创建成功单例类之前,是不存在map.put
的过程,自然也不会出现前者的情况。