JDK BUG

这篇文章,聊一下我最近才知道〖的〗一个关于 JDK 8 〖的〗 BUG 吧。

【首先】[说一下我是怎么发现这个 BUG 〖的〗呢?

人人都知道我对 Dubbo 有一定〖的〗关注,前段时间 Dubbo 2.7.7 公布后我看<了>它〖的〗更新点,就是下面这个网址: https://github.com/apache/dubbo/releases/tag/dubbo-2.7.7

其中有 Bugfixes 这一部门:

每一个我都去简朴〖的〗看<了>一下,其他〖的〗 Bugfixes 或多或少都和 Dubbo 框架有一定〖的〗关联性。然则上面红框框起来〖的〗部门完全就是 JDK 〖的〗 Bug <了>。

《以是》可以单独拎出来说。

这个 Bug 我也是看到<了>这个地刚刚知道〖的〗,然则研究〖的〗历程中我发现,这个怎么说呢:我嫌疑这根本就不是 Bug ,这就是 Doug Lea 老爷子在(钓鱼执法)。

为什么这样〖的〗说呢,人人看完本文就知道<了>。

Bug 稳固复现

点击 Dubbo 内里〖的〗链接,我们可以看到详细〖的〗形貌就是一个链接:

打开这个链接:

https://bugs.openjdk.java.net/browse/JDK-8062841

我们可以看到:这个 Bug 是位于赫赫有名〖的〗 concurrent 包内里〖的〗 computeIfAbsent 「方式」。

这个 Bug 在 JDK 9 内里被修复<了>,修复人是 Doug Lea。

而我们知道 ConcurrentHashMap 就是 Doug Lea 〖的〗大作,可以说是“谁污染谁治理”。

要领会这个 Bug 是怎么回事,就必须先领会下面这个「方式」是干啥〖的〗:

java.util.concurrent.ConcurrentHashMap#computeIfAbsent

从这个「方式」〖的〗第二个入参 mappingFunction 我们可以知道这是 JDK 8 【之后】提供〖的〗「方式」<了>。

该「方式」〖的〗寄义是:当前 Map 中 key 对应〖的〗值不存在时,会挪用 mappingFunction 函数,而且将该函数〖的〗执行效果(不为 null)作为该 key 〖的〗 value 返回。

好比下面这样〖的〗:

初始化一个 ConcurrentHashMap ,然后第一次去获取 key 为 why 〖的〗 value,没有获取到,〖直接返回〗 null。

接着挪用 computeIfAbsent 「方式」,获取到 null 后挪用 getValue 「方式」,将该「方式」〖的〗返回值和当前〖的〗 key 关联起来。

《以是》,第二次获取〖的〗时刻拿到<了> “why手艺”。

实在上面〖的〗代码〖的〗 17 行〖的〗返回值就是 “why手艺”,只是我为<了>代码演示,再去挪用<了>一次 map.get() 「方式」。

知道这个「方式」干什么〖的〗,《接下来》就带人人看看 Bug 是什么。

我们直接用这个问题内里〖给〗〖的〗《测试用例》,地址:

https://bugs.openjdk.java.net/secure/attachment/23985/Main.java

‘我’只是在第 11 行和第 21 行加入<了>输出语句:

正常〖的〗情形下,我们希望「方式」正常竣事,然后 map 内里是这样〖的〗:{AaAa=42,BBBB=42}

然则你把这个代码拿到内陆去跑(需要 JDK 8 环境),你会发现,这个「方式」永远不会竣事。由于它在举行死循环。

这就是 Bug。

提问〖的〗艺术

知道 Bug <了>,按理来说就应该最先剖「析源码」,领会为啥泛起<了>会泛起这个 Bug。

然则我想先插播一小节提问〖的〗艺术。由于这个 Bug 就是一个活生生〖的〗示例呀。

这个链接,我建议你打开看看,这内里另有 Doug Lea 老爷子〖的〗亲自解答:

https://bugs.openjdk.java.net/browse/JDK-8062841

【首先】[我们看提出问题〖的〗这小我私家对于问题〖的〗形貌(可以先不用细看,横竖看着也是懵逼〖的〗):

通常情形下,被提问〖的〗人分为两类人:

1.遇到过并知道这个问题〖的〗人,可以看〖的〗明<了>你在说什么。

2.虽然没有碰见过这个问题,但感受是自己熟悉〖的〗领域,可能知道谜底,然则看<了>你〖的〗问题形貌,也不知道你在说什么。

这个形貌很长,我第一次看〖的〗时刻很懵逼,很难明白他在说什么。我就是属于第二类人。

而且在大多数〖的〗问题中,第二类人比第一类人多〖许多〗。

然则当我领会到这个 Bug 〖的〗前因后果〖的〗时刻,再看这个形貌,实在写〖的〗很清晰<了>,也很好明白。我就酿成第一类人<了>。

然则酿成第一类人是有条件〖的〗,条件就是我已经领会到<了>这个地方 Bug <了>。惋惜,现在是提问,而被提问〖的〗人,还对这个 Bug 不是稀奇领会。

纵然,这个被提问〖的〗人是 Doug Lea。

可以看到,2014 年 11 月 04 日 Martin 提出这个问题后, Doug Lea 在不到一个小时内就举行<了>回复,我〖给〗人人翻译一下,老爷子回复〖的〗啥:

【首先】[,你说你发现<了> ConcurrentHashMap 〖的〗问题,然则我没有看到〖的〗《测试用例》。(那么我)就预测一下是不是有其他线程在盘算值〖的〗时刻被卡住<了>,“然则从你〖的〗形”貌中我也看不到响应〖的〗点。

简朴来说就是:Talk is cheap. Show me the code.(屁话少说,放码过来。)

于是另一个哥们 Pardeep 在一个月后提交<了>一个测试案例,就是我们前面看到〖的〗测试案例:

Pardeep 〖给〗 Martin 回复到下面这段话:

他直言不讳〖的〗说:我注重这个 bug 很长时间<了>,然后我另有一个《测试用例》。

可以说这个测试案例〖的〗泛起,才是真正〖的〗转折点。

然后他提出<了>自己〖的〗看法,这段形貌简短有力〖的〗说出<了>问题〖的〗所在(后面我们会讲到),然后他还提出<了>自己〖的〗意见。

不到一个小时,这个回到得到<了> Doug Lea 〖的〗回复:

他说:小伙子〖的〗建议照样不错〖的〗,然则现在还不是我们解决这个问题〖的〗时刻。我们也许会通过代码改善死锁检查机制,以辅助用户 debug 他们〖的〗程序。然则现在而言,这种机制就算做出来,事情效率也是异常低下〖的〗,好比在当前〖的〗这个案例下。然则现在我们至少清晰〖的〗知道,是否要实现这种机制是不能确定〖的〗。

总之一句话:问题我知道<了>,然则现在我还没想到好〖的〗解决「方式」。

然则,在 19 天以后,老爷子又回来处置这个问题<了>:

这次〖的〗回覆可谓是峰回路转,他说:请忽略我之前〖的〗话。我们发现<了>一些可行〖的〗改善「方式」,这些改善可以处置更多〖的〗用户错误,包罗本讲述中所提供〖的〗《测试用例》,即解决在 computeIfAbsent 中提供〖的〗函数中举行递归映射更新导致死锁这样〖的〗问题。我们会在 JDK 9 内里解决这个问题。

《以是》,回首这个 Bug 被提出〖的〗历程。

【首先】[是 Martin 提出<了>这个问题,并举行<了>详细〖的〗形貌。惋惜〖的〗是他〖的〗形貌很专业,是站在你已经领会<了>这个 Bug 〖〖的〗〗态度上去形貌〖的〗,让人看〖的〗很懵逼。

《以是》 Doug Lea 看到后也示意这啥呀,没搞懂。

然后是 Pardeep 跟进这个问题,转折点在于他抛出〖的〗这个测试案例。而我信赖,既然 Martin 能把这个问题形貌〖的〗很清晰,他一定是有一个自己〖的〗测试案例〖的〗,然则他没有展现出来。

《以是》,朋友们,测试案例〖的〗重要性不言而喻<了>。问问题〖的〗时刻不要只是抛出异常,你至少〖给〗段对应〖的〗代码,或者日志,或者一次性形貌清晰,写在文档内里发出来也行呀。

Bug 〖的〗缘故原由

导致这个 Bug 〖的〗缘故原由也是一句话就能说清晰,前面〖的〗 Pardeep 老哥也说<了>:

问题在于我们在举行 computeIfAbsent 〖的〗时刻,内里另有一个 computeIfAbsent。而这两个 computeIfAbsent 它们〖的〗 key 对应〖的〗 hashCode 是一样〖的〗。

你说巧不巧。

当它们〖的〗 hashCode ‘是一样〖的〗时刻’,说明它们要往同一个槽放器械。

而当第二个元素进来〖的〗时刻,发现坑位已经被前一个元素占领<了>,可能就是这样〖的〗画风:

接下来我们就剖析一下 computeIfAbsent 「方式」〖的〗事情流程:

第一步是盘算 key 对应〖的〗 hashCode 应该放到哪个槽内里。

然后是进入1649 行〖的〗这个 for 循环,而这个 for 循环是一个死循环,它在循环体内部判断种种情形,若是知足条件则 break 循环。

【首先】[,我们看一下 “AaAa” 和 “BBBB” 经由 spread 盘算(右移 16 位高效盘算)后〖的〗 h 值是什么:

哇塞,好巧啊,从框起来〖的〗这两部门可以看到,都是 2031775 呢。

说明他们要在同一个槽内里搞事情。

先是 “AaAa” 进入 computeIfAbsent 「方式」:

在第一次循环〖的〗时刻 initTable,没啥说〖的〗。

第二次循环先是在 1653 行盘算出数组〖的〗下标,并取出该下标〖的〗 node。发现这个 node 是空〖的〗。于是进入分支判断:

在标号为 ① 〖的〗地方举行 cas 操作,先用 r(即 ReservationNode)举行一个占位〖的〗操作。

在标号为 ② 〖的〗地方举行 mappingFunction.apply 〖的〗操作,盘算 value 值。若是盘算出来不为 null,则把 value 组装成最终〖的〗 node。

在标号为 ③ 〖的〗器械把之前占位〖的〗 ReservationNode 替换成标号为 ② 〖的〗地方组装成〖的〗node 。

问题就泛起标号为 ② 〖的〗地方。可以看到这里去举行<了> mappingFunction.apply 〖的〗操作,而这个操作在我们〖的〗案例下,会触发另一次 computeIfAbsent 操作。

现在 “AaAa” 就等着这个 computeIfAbsent 操作〖的〗返回值,然后举行下一步操作,《也就是举行标号为》 ③ 〖的〗操作<了>。

接着 “BBBB” 就来<了>。

通过前面我们知道<了> “BBBB” 〖的〗 hashCode 经由盘算后也是和 “AaAa” 一样。《以是》它也要想要去谁人槽内里搞事情。

惋惜它来晚<了>一步。

“带人人看一下对应〖的〗代”码:

当 key 为 “BBBB” 〖的〗时刻,算出来〖的〗 h 值也是 2031775。

它也会进入 1649 行〖的〗这个死循环。然后举行种种判断。

接下来我要论证〖的〗是:

(在本文〖的〗示例代码中),当运行到 key 为 “BBBB” 〖的〗时刻,进入 1649 行这个死循环后,就退不出来<了>。程序一直在内里循环运行。

在标号为 ① 〖的〗地方,由于这个时刻 tab 已经不为 null <了>,《以是》不会进入这个分支。

在标号为 ② 〖的〗地方,由于之前 “AaAa” 已经扔<了>一个 ReservationNode 进去占位置<了>,《以是》不等于 null。《以是》,也就不会进入这个分支。

怕你懵逼,〖给〗你配个图,真是暖男作者石锤<了>:

接下来到标号为 ③ 〖的〗地方,内里有一个 MOVED,这个 MOVED 【是干啥〖的〗呢】?

示意当前〖的〗 ConcurrentHashMap 是否是在举行扩容。

很明显,现在还没有到该扩容〖的〗时刻:

第 1678 行〖的〗 f 就是之前 “AaAa” 扔进去〖的〗 ReservationNode ,这个 Node 〖的〗 hash 是 -3,不等于MOVED(-1)。

《以是》,不会进入这个分支判断。

接下来,能进〖的〗只有标号为 ④ 〖的〗地方<了>,《以是》我们只需要把这个地方攻破,就彻底领会这个 Bug <了>。

走起:

通过前面〖的〗剖析我们知道<了>,当前案例情形下,只会进入 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

你去翻一翻。发现,啊,全是知识点啊,学不动<了>。

(钓鱼执法)

为什么我在文章〖的〗一最先就说<了>这是 Doug Lea 在(钓鱼执法)呢?

由于在最最先提问〖的〗艺术那一部门,我信赖,Doug Lea 跑完谁人测试案例【之后】,心里也有点数<了>。

也许知道问题在哪<了>,而且从他〖的〗回覆和他写〖的〗文档中我也有理由信赖,他写〖的〗这个「方式」〖的〗时刻就知道可能会出问题。

而且,Pardeep 〖的〗回复中提到<了>文档,那我们就去看看官方文档对于该「方式」〖的〗形貌是怎样〖的〗:

https://docs.oracle.com/javase/8/docs/api/

文档中说函数「方式」应该简短,简朴。而且不能在更新〖的〗映射〖的〗时刻更新映射。就是说不能套娃。

套娃,用程序说就是recursive(递归),凭据文档说若是存在递归,则会抛出 IllegalStateException 。

而提到递归,你想到<了>什么?

我 【首先】[就想到<了>斐波拉契函数。我们用 computeIfAbsent 实现一个斐波拉契函数如下:

public class Test {

static Map<Integer, Integer> cache = new ConcurrentHashMap<>();

    public static void main(String[] args) {
        System.out.println("f(" + 14 + ") =" + fibonacci(14));
    }

    static int fibonacci(int i) {
        if (i == 0)
            return i;
        if (i == 1)
            return 1;
        return cache.computeIfAbsent(i, (key) -> {
            System.out.println("Slow calculation of " + key);
            return fibonacci(i - 2) + fibonacci(i - 1);
        });
    }
}

这就是递归挪用,我用 JDK 1.8 跑〖的〗时刻并没有抛出 IllegalStateException,只是程序假死<了>,缘故原由和我们前面剖析〖的〗是一样一样〖的〗。我明白这个地方是和文档不符〖的〗。

《以是》,我嫌疑是 Doug Lea 在这个地方(钓鱼执法)。

CHM一定线程平安吗?

既然都说到 currentHashMap(CHM)<了>,那我说一个相关〖的〗注重点吧。

【首先】[ CHM 一定能保证线程平安吗?

是〖的〗,CHM 自己一定是线程平安〖的〗。然则,若是你使用不当照样有可能会泛起线程不平安〖的〗情形。

〖给〗人人看一点 Spring 中〖的〗源码吧:

org.springframework.core.SimpleAliasRegistry

在这个类中,aliasMap 是 ConcurrentHashMap 类型〖的〗:

在 registerAlias 和 getAliases 「方式」中,都有对 aliasMap 举行操作〖的〗代码,然则在操作之前都是用 synchronized 把 aliasMap 锁住<了>。

为什么?为什么我们操作 ConcurrentHashMap 〖的〗时刻还要加锁呢?

这个是凭据场景而定〖的〗,这个别名管理器,在这里加锁应该是为<了>制止多个线程操作 ConcurrentHashMap 。

虽然 ConcurrentHashMap 是线程平安〖的〗,然则假设若是一个线程 put,一个线程 get,在这个代码〖的〗场景内里是不允许〖的〗。

若是以为不太好明白〖的〗(话我举一个) redis 〖的〗例子。

redis 〖的〗 get、set 「方式」都是线程平安〖的〗吧。然则你若是先 get 再 set,那么在多线程〖的〗情形下照样会有问题〖的〗。

由于这两个操作不是原子性〖的〗。《以是》 incr 就应运而生<了>。

我举这个例子〖的〗是想说线程平安与否不是绝对〖的〗,要看场景。〖给〗你一个线程平安〖的〗容器,你使用不当照样会有线程平安〖的〗问题。

再好比,HashMap 一定是线程不平安〖的〗吗?

说不能说〖的〗这么死吧。它是一个线程不平安〖的〗容器。然则若是我〖的〗使用场景是只读呢?

在这个只读〖的〗场景下,它就是线程平安〖的〗。

总之,看场景。原理,就是这么一个原理。

最后说两句(求关注)

《以是》点个“赞”吧,周更很累〖的〗,不要白嫖我,需要一点正反馈。

才疏学浅,难免会有纰漏,若是你发现<了>错误〖的〗地方,还请你留言指出来,我对其加以修改。

谢谢您〖的〗阅读,我坚持原创,十分迎接并谢谢您〖的〗关注。

我是 why,一个被代码延迟〖的〗文学创作者,不是大佬,然则喜欢分享,是一个又暖又有料〖的〗四川好男子。

迎接关注我〖的〗微信民众号:why手艺。在这里我会分享一些java手艺相关〖的〗知识,用匠心敲代码,对每一行代码卖力。偶然也会荒腔走板〖的〗聊一聊生涯,写一写书评、影评。谢谢你〖的〗关注,愿你我共同进步。

,

欧博亚洲APP下载

欢迎进入欧博亚洲APP下载(Allbet Game):www.aLLbetgame.us,欧博官网是欧博集团〖的〗官方网站。欧博官网开放Allbet「注册」、Allbe代理、Allbet电脑客户端、Allbet手机版下载等业务。

Allbet Gaming声明:该文看法仅代表作者自己,与阳光在线无关。转载请注明:联博统计接口:震惊!ConcurrentHashMap内里也有死循环,作者留下的“彩蛋”领会一下?
发布评论

分享到:

太原房产网:小白学 Python 数据分析(18):Matplotlib(三)(常用图表)(上)
1 条回复
  1. 环球UG官网
    环球UG官网
    (2020-08-16 00:08:55) 1#

    Allbet注册欢迎进入Allbet注册(Allbet Game):www.aLLbetgame.us,欧博官网是欧博集团的官方网站。欧博官网开放Allbet注册、Allbe代理、Allbet电脑客户端、Allbet手机版下载等业务。好酷

发表评论

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。