走起:
通过前面的分析我们知道了,当前案例情况下,只会进入 1672 行这个分支。
而这个分支里面,还有四个判断。我们一个个的攻破:
标号为 ⑤ 的地方,tabAt 方法取出来的对象,就是之前 “AaAa” 放进去的占位的 ReservationNode ,也就是这个 f 。所以可以进入这个分支判断。
标号为 ⑥ 的地方,fh >=0 。而 fh 是当前 node 的 hash 值,大于 0 说明当前是按照链表存储的数据。之前我们分析过了,当前的 hash 值是 -3。所以,不会进入这个分支。
标号为 ⑦ 的地方,判断 f 节点是否是红黑树存储。当然不是的。所以,不会进入这个分支。
标号为 ⑧ 的地方,binCount 代表的是该下标里面,有几个 node 节点。很明显,现在一个都没有。所以当前的 binCount 还是 0 。所以,不会进入这个分支。
完了。分析完了。
Bug 也就出来了,一次 for 循环结束后,没有 break。苦就苦在这个 for 循环还是个死循环。
再来一个上帝视角,看看当 key 为 “BBBB” 的时候发生了什么事情:
进入无限循环内:
①.经过 “AaAa” 之后,tab 就不为 null 了。
②.当前的槽中已经被 “AaAa” 先放了一个 ReservationNode 进行占位了,所以不为 null。
③.当前的 map 并没有进行扩容操作。
④.包含⑤、⑥、⑦、⑧。
⑤.tabAt 方法取出来的对象,就是之前 “AaAa” 放进去的占位的 ReservationNode,所以满足条件进入分支。
⑥.判断当前是否是链表存储,不满足条件,跳过。
⑦.判断当前是否是红黑树存储,不满足条件,跳过。
⑧.判断当前下标里面是否放了 node,不满足条件(“AaAa” 只有个占位的Node ,并没有初始完成,所以还没有放到该下标里面),进入下一次循环。
然后它就在死循环里面出不来了!
我相信现在大家对于这个 Bug 的来路了解清楚了。
如果你是在 idea 里面跑这个测试用例,也可以这样直观的看一眼:
点击这个照相机图标:
从线程快照里面其实也是可以看到端倪的,大家可以去分析分析。
有的观点说的是由于线程安全的导致的死循环,经过分析我觉得这个观点是不对的。
它存在死循环,不是由于线程安全导致的,纯粹是自己进入了死循环。
或者说,这是一个“彩蛋”?
或者......自信点,就说这事 Bug ,能稳定复现的那种。
那么我们如果是使用 JDK 8 怎么避免踩到这个“彩蛋”呢?
看看 Dubbo 里面是怎么解决的:
先调用了 get 方法,如果返回为 null,则调用 putIfAbsent 方法,这样就能实现和之前一样的效果了。
如果你在项目中也有使用 computeIfAbsent 的地方,建议也这样去修改。
说到 ConcurrentHashMap get 方法返回 null,我就想起了之前讨论的一个面试题了:
答案都写在这个文章里面了,有兴趣的可以了解一下《这道面试题我真不知道面试官想要的回答是什么》
Bug 的解决 其实彻底理解了这个 Bug 之后,我们再来看一下 JDK 9 里面的解决方案,看一下官方源码对比:
http://gee.cs.oswego.edu/cgi-bin/viewcvs.cgi/jsr166/src/main/java/util/concurrent/ConcurrentHashMap.java?r1=1.258&r2=1.259&sortby=date&diff_format=f
就加了两行代码,判断完是否是红黑树节点后,再判断一下是否是 ReservationNode 节点,因为这个节点就是个占位节点。如果是,则抛出异常。
就这么简单。没有什么神秘的。
所以,如果你在 JDK 9 里面执行文本的测试用例,就会抛出 IllegalStateException
这就是 Doug Lea 之前提到的解决方案:
了解了这个 Bug 的来龙去脉后,特别是看到解决方案后,我们就能轻描淡写的说一句:
害,就这?没听说过!
另外,我看 JDK 9 修复的时候还不止修复了一个问题:
http://hg.openjdk.java.net/jdk9/jdk9/jdk/file/6dd59c01f011/src/java.base/share/classes/java/util/concurrent/ConcurrentHashMap.java
你去翻一翻。发现,啊,全是知识点啊,学不动了。