你好呀,我是歪歪。
这次给你盘一个特别有意思的源码,正如我标题说的那样:看懂这行源码之后,我不禁兴起掌来,直呼祖师爷牛逼。
这行源码是这样的:
java.util.concurrent.LinkedBlockingQueue#dequeue
h.next = h,不过是一个把下一个节点指向自己的动作罢了。
这行代码后边的注释“help GC”其实在 JDK 的源码里边也随处可见。
不论怎么看都是一行平平无奇的代码和随处可见的注释罢了。
可是这行代码背面隐藏的故事,可就太有意思了,真的牛逼,儿豁嘛。
它在干啥。
首要,咱们得先知道这行代码所在的办法是在干啥,然后再去剖析这行代码的作用。
所以老规矩,先搞个 Demo 出来跑跑:
在 LinkedBlockingQueue 的 remove 办法中就调用了 dequeue 办法,调用链路是这样的:
这个办法在 remove 的过程中承当一个什么样的角色呢?
这个问题的答案能够在办法的注释上找到:
这个办法便是从行列的头部,删去一个节点,其他啥也不干。
就拿 Demo 来说,在履行这个办法之前,咱们先看一下其时这个链表的状况是怎么样的:
这是一个单向链表,然后 head 结点里边没有元素,即 item=null,对应做个图出来便是这样的:
当履行完这个办法之后,链表变成了这样:
再对应做个图出来,便是这样的:
能够发现 1 没了,由于它是真实的“头节点”,所以被 remove 掉了。
这个办法就干了这么一个事儿。
尽管它一共也只有六行代码,可是为了让你更好的入戏,我决定先给你逐行解说一下这个办法的代码,讲着讲着,你就会发现,诶,问题它就来了。
首要,咱们回到办法入口处,也便是回到这个时分:
前两行办法是这样的:
对应到图上,也便是这样的:
- h 对应的是 head 节点
- first 对应的是 “1” 节点
然后,来到第三行:
h 的 next 仍是 h,这便是一个自己指向自己的动作,对应到图上是这样的:
然后,第四行代码:
把 first 变成 head:
最终,第五行和第六行:
拿到 first 的 item 值,作为办法的回来值。然后再把 first 的 item 值设置为 null。
对应到图中便是这样,第五行的 x 便是 1,第六行履行完结之后,图就变成了这样:
整个链表就变成了这样:
那么现在问题来了:
假如咱们没有 h.next=h 这一行代码,会呈现什么问题呢?
我也不知道,可是咱们能够推演一下:
也便是终究咱们得到的是这样的一个链表:
这个时分咱们发现,由于 head 指针的方位现已产生了改变,并且这个链表又是一个单向链表,所以当咱们使用这个链表的时分,没有任何问题。
而这个目标:
现已没有任何指针指向它了,那么它不通过任何处理,也是能够被 GC 收回掉的。
对吗?
你细细的品一品,是不是这个道理,从 GC 的角度来说它的确是“不可达了”,的确能够被收回掉了。
所以,其时有人问了我这样的一个问题:
我通过上面的一顿剖析,发现:嗯,的确是这样的,的确没啥卵用啊,不写这一行代码,功能也是完结正常的。
可是其时我是这样回复的:
我没有把话说满,由于这一行成心写了一行“help GC”的注释,或许有 GC 方面的考虑。
那么到底有没有 GC 方面的考虑,是怎么考虑的呢?
凭借着我这几年写文章的敏锐嗅觉,我觉得这儿“大有文章”,所以我带着这个问题,在网上散步了一圈,还真有收成。
help GC?
首要,一顿搜索,扫除了无数个无关的头绪之后,我在 openjdk 的 bug 列表里边定位到了这样的一个链接:
bugs.openjdk.org/browse/JDK-…
点击进这个链接的原因是标题其时就把吸引到了,翻译过来便是说:LinkedBlockingQueue 的节点应该在成为“废物”之前解除自己的链接。
先不论啥意思吧,横竖 LinkedBlockingQueue、Nodes、unlink、garbage 这些关键词是完全对上了。
所以我看了一下描绘部分,首要关怀到了这两个部分:
看到标号为 ① 的当地,我才发现在 JDK 6 里边对应完成是这样的:
并且其时的办法仍是叫 extract 而不是 dequeue。
这个办法称号的改变,也算是一处小细节吧。
dequeue 是一个更加专业的叫法:
细心看 JDK 6 中的 extract 办法,你会发现,根本就没有 help GC 这样的注释,也没有相关的代码。
它的完成办法便是我前面画图的这种:
也便是说这行代码一定是出于某种原因,在后边的 JDK 版别中加上的。那么为什么要进行标号为 ① 处那样的修改呢?
标号为 ② 的当地给到了一个链接,说是这个链接里边有关于这个问题深化的评论。
For details and in-depth discussion, see:
thread.gmane.org/gmane.comp.…
我十分坚信我找对了当地,并且我要寻觅的答案就在这个链接里边。
可是当我点过去的时分,我发现不论怎么拜访,这个链接拜访不到了…
尽管这儿的头绪断了,可是顺藤摸瓜,我找到了这个 BUG 链接:
bugs.openjdk.org/browse/JDK-…
这两个 BUG 链接说的其实是同一个工作,可是这个链接里边给了一个示例代码。
这个代码比较长,我给你截个图,你先不用细看,仅仅比照我框起来的两个部分,你会发现这两部分的代码其实是相同的:
当 LinkedBlockingQueue 里边参加了 h.next=null 的代码,跑上面的程序,输出结果是这样:
可是,当 LinkedBlockingQueue 使用 JDK 6 的源码跑,也便是没有 h.next=null 的代码跑上面的程序,输出结果是这样:
产生了 47 次 FGC。
这个代码,在我的电脑上跑,我用的是 JDK 8 的源码,然后注释掉 h.next = h 这行代码,仅仅会触发一次 FGC,时刻距离是 2 倍:
加上 h.next = h,两次时刻就相对安稳:
好,到这儿,不论原理是什么,咱们至少验证了,在这个当地必需求 help GC 一下,不然的确会有性能影响。
可是,到底是为什么呢?
在重复细心的阅览了这个 BUG 的描绘部分之后,我大概懂了。
最关键的一个点其实是藏在了前面示例代码中我标示了五角星的那一行注释:
SAME test, but create the queue before GC, head node will be in old gen(头节点会进入老时代)
我大概知道问题的原因是由于“head node will be in old gen”,可是详细让我描绘出来我也有点说不出来。
说人话便是:我懂一点,可是不多。
所以又通过一番查找,我找到了这个链接,在这儿面完全搞理解是怎么一回事了:
concurrencyfreaks.blogspot.com/2016/10/sel…
在这个链接里边说到了一个视频,它让我从第 23 分钟开端看:
我看了一下这个视频,应该是 2015 年发布的。由于整个会议的主题是:20 years of Java, just the beginning:
www.infoq.com/presentatio…
这个视频的主题是叫做“Life if a twitter JVM engineer”,是一个 twitter 的 JVM 工程师在大会共享的在作业遇到的一些关于 JVM 的问题。
尽管是全程英文,可是你知道的,我的 English level 仍是比较 high 的。
日常传闻,问题不大。所以大概也就听了个几十遍吧,结合着他的 PPT 也就知道关于这个部分他到底在共享啥了。
我要寻觅的答案,也藏在这个视频里边。
我挑关键的给你说。
首要他展示了这样的这个图片:
老时代的 x 目标指向了年青代的 y 目标。一个十分简略的示意图,他首要是想要表达“跨代引证”这个问题。
然后,呈现了这个图片:
这儿的 Queue 便是本文中评论的 LinkedBlockingQueue。
首要能够看到整个 Queue 在老时代,作为一个行列目标,极有或许生命周期比较长,所以行列在老时代是一个正常的现象。
然后咱们往这个行列里边刺进了 A,B 两个元素,由于这两个元素是咱们刚刚刺进的,所以它们在年青代,也没有任何缺点。
此刻就呈现了老时代的 Queue 目标,指向了坐落年青代的 A,B 节点,这样的跨代引证。
接着,A 节点被干掉了,出队:
A 出队的时分,由于它是在年青代的,且没有任何老时代的目标指向它,所以它是能够被 GC 收回掉的。
同理,咱们刺进 D,E 节点,并让 B 节点出队:
假定此刻产生一次 YGC, A,B 节点由于“不可达”被干掉了,C 节点在经历几回 YGC 之后,由于不是“废物”,所以提升到了老时代:
这个时分假定 C 出队,你说会呈现什么状况?
首要,我问你:这个时分 C 出队之后,它是否是废物?
必定是的,由于它不可达了嘛。从图片上也能够看到,C 尽管在老时代,可是没有任何目标指向它了,它的确完犊子了:
好,接下来,请坐好,细心听了。
此刻,咱们参加一个 F 节点,没有任何缺点:
接着 D 元素被出队了:
就像下面这个动图相同:
我把这一帧拿出来,针对这个 D 节点,独自的说:
假定在这个时分,再次产生 YGC,D 节点尽管出队了,它也坐落年青代。可是坐落老时代的 C 节点还指向它,所以在 YGC 的时分,废物收回线程不敢动它。
因此,在几轮 YGC 之后,原本是“废物”的 D,摇身一变,进入老时代了:
尽管它依然是“废物”,可是它进入了老时代,YGC 对它束手无策,得 FGC 才干干掉它了。
然后越来越多的出队节点,变成了这样:
然后,他们都进入了老时代:
咱们站在天主视角,咱们知道,这一串节点,应该在 YGC 的时分就被收回掉。
可是这种状况,你让 GC 怎么处理?
它根本就处理不了。
GC 线程没有天主视角,站在它的视角,它做的每一步动作都是正确的、符合规定的。终究呈现的效果便是必需求经历 FGC 才干把这些原本早就应该收回的节点,进行收回。而咱们知道,FGC 是应该尽量避免的,所以这个处置方案,仍是“差点意思”的。
所以,咱们应该怎么办?
你回想一下,万恶之源,是不是这个时分:
C 尽管被移出行列了,可是它还持有一个下一个节点的引证,让这个引证变成跨代引证的时分,就出缺点了。
所以,help GC,这不就来了吗?
不论你是坐落年青代仍是老时代,只要是出队,就把你的 next 引证干掉,杜绝呈现前面咱们剖析的这种状况。
这个时分,你再回过头去看前面说到的这句话:
head node will be in old gen…
你就应该懂得起,为什么 head node 在 old gen 就要出事儿。
h.next=null ???
前面一节,通过一顿剖析之后,知道了为什么要有这一行代码:
可是你细心一看,在咱们的源码里边是 h.hext=h 呀?
并且,通过前面的剖析咱们能够知道,理论上,h.next=null 和 h.hext=h 都能到达 help GC 的目的,那么为什么终究的写法是 h.hext=h 呢?
或者换句话说:为什么是 h.next=h,而不是 h.next=null 呢?
针对这个问题,我也盯着源码,细心考虑了好久,终究得出了一个“十分大胆”的结论是:这两个写法是相同的,不过是编码习气不相同罢了。
可是,留意,我要说可是了。
再次通过一番查询、剖析和论证,这个当地它还必须得是 h.next=h。
由于在这个 bug 下面有这样的一句评论:
关键词是:weakly consistent iterator,弱一致性迭代器。也便是说这个问题的答案是藏在 iterator 迭代器里边的。
在 iterator 对应的源码中,有这样的一个办法:
java.util.concurrent.LinkedBlockingQueue.Itr#nextNode
针对 if 判别中的 s==p,咱们把 s 替换一下,就变成了 p.next=p:
那么什么时分会呈现 p.next=p 这样的代码呢?
答案就藏在这个办法的注释部分:dequeued nodes (p.next == p)
dequeue 这不是巧了吗,这不是和前面给呼应起来了吗?
好,到这儿,我要开端给你画图说明了,假定咱们 LinkedBlockingQueue 里边放的元素是这样的:
画图出来便是这样的:
现在咱们要对这个链表进行迭代,对应到画图便是这样的:
linkedBlockingQueue.iterator();
看到这个图的时分,问题就来了:current 指针是什么时分冒出来的呢?
current,这个变量是在生成迭代器的时分就初始化好了的,指向的是 head.next:
然后 current 是通过 nextNode 这个办法进行维护的:
正常迭代下,每调用一次都会回来 s,而 s 又是 p.next,即下一个节点:
所以,每次调用之后 current 都会移动一格:
这种状况,完全就没有这个分支的事儿:
什么时分才会和它扯上联系呢?
你幻想一个场景。
A 线程刚刚要对这个行列进行迭代,而 B 线程同时在对这个行列进行 remove。
关于 A 线程,刚刚开端迭代,画图是这样的:
然后 current 还没开端移动呢,B 线程“咔咔”几下,直接就把 1,2,3 全部给干出行列了,所以站在 B 线程的视角,行列是这样的了:
到这儿,你先考虑一个问题:1,2,3 这几个节点,不论是自己指向自己,仍是指向一个 null,此刻产生一个 YGC 它们还在不在?
2 和 3 指定是没了,可是 1 可不能被收回了啊。
由于尽管元素为 1 的节点出队了,可是站在 A 线程的视角,它还持有一个 current 引证呢,它仍是“可达”的。
所以,这个时分 A 线程开端迭代,尽管 1 被 B 出队了,可是它相同会被输出。
然后,咱们再来关于下面这两种状况,A 线程会怎么进行迭代:
当 1 节点的 next 指为 null 的时分,即 p.next 为 null,那么满足 s==null 的判别,所以 nextNode 办法就会回来 s,也便是回来了 null:
当你调用 hasNext 办法判别是否还有下一节点的时分,就会回来 false,循环就完毕了:
然后,咱们站在天主视角是知道的,后边还有 4 和 5 没输出呢,所以这样就会呈现问题。
可是,当 1 节点的 next 指向自己的时分,有趣的工作就来了:
current 指针就变成了 head.next。
而你看看其时的这个链表里边 head.next 是啥?
不便是 4 节点吗?
这不就衔接上了吗?
所以终究 A 线程会输出 1,4,5。
尽管咱们知道 1 元素其完成已出队了,可是 A 线程开端迭代的时分,它至少还在。
这玩意就体现了前面说到的: weakly consistent iterator,弱一致性迭代器。
这个时分,你再结合者迭代器上的注解去看,就能搞得明理解白了:
假如 hasNext 办法回来为 true,那么就必需求有下一个节点。即便这个节点被比方 take 等等的办法给移除了,也需求回来它。这便是 weakly-consistent iterator。
然后,你再看看整个类开端部分的 Java doc,其实我整篇文章便是关于这一段描绘的翻译和扩大:
看完并了解我这篇文章之后,你再去看这部分的 Java doc,你就知道它是在说个啥工作,以及它为什么要这样的去做这件工作了。
好了,看到这儿,你现在应该理解了,为什么必需求有 h.next=h,为什么不能是 h.next=null 了吧?
理解了就好。
由于本文就到这儿就要完毕了。
假如你还没理解,不要怀疑自己,大胆的说出来:什么玩意?写的弯弯绕绕的,看求不懂。呸,废物作者。
最终,我还想要说的是,关于 LBQ 这个行列,我之前也写过这篇文章专门说它:《喜提JDK的BUG一枚!多线程的状况下请谨慎使用这个类的stream遍历。》
文章里边也说到了 dequeue 这个办法:
可是其时我完全没有考虑到文本说到的问题,顺着代码就捋过去了。
我觉得看到这部分代码,然后能提出本文中这两个问题的人,才是在带着自己考虑深度阅览源码的人。
解决问题不厉害,提出问题才是最屌的,由于当一个问题提出来的时分,它就现已被解决了。
带着质疑的眼光看代码,带着求真的态度去探索,与君共勉之。