关于后端:我试图通过这篇文章告诉你这行源码有多牛逼

2次阅读

共计 6395 个字符,预计需要花费 16 分钟才能阅读完成。

你好呀,我是歪歪。

这次给你盘一个特地有意思的源码,正如我题目说的那样:看懂这行源码之后,我不禁鼓起掌来,直呼祖师爷牛逼。

这行源码是这样的:

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 列表外面定位到了这样的一个链接:

https://bugs.openjdk.org/browse/JDK-6805775

点击进这个链接的起因是题目过后就把吸引到了,翻译过去就是说:LinkedBlockingQueue 的节点应该在成为“垃圾”之前解除本人的链接。

先不论啥意思吧,反正 LinkedBlockingQueue、Nodes、unlink、garbage 这些关键词是齐全对上了。

于是我看了一下形容局部,次要关怀到了这两个局部:

看到标号为 ① 的中央,我才发现在 JDK 6 外面对应实现是这样的:

而且过后的办法还是叫 extract 而不是 dequeue。

这个办法名称的变动,也算是一处小细节吧。

dequeue 是一个更加业余的叫法:

认真看 JDK 6 中的 extract 办法,你会发现,基本就没有 help GC 这样的正文,也没有相干的代码。

它的实现形式就是我后面画图的这种:

也就是说这行代码肯定是出于某种原因,在前面的 JDK 版本中加上的。那么为什么要进行标号为 ① 处那样的批改呢?

标号为 ② 的中央给到了一个链接,说是这个链接外面有对于这个问题深刻的探讨。

For details and in-depth discussion, see:
http://thread.gmane.org/gmane.comp.java.jsr.166-concurrency/5758

我十分确信我找对了中央,而且我要寻找的答案就在这个链接外面。

然而当我点过来的时候,我发现不管怎么拜访,这个链接拜访不到了 …

尽管这里的线索断了,然而顺藤摸瓜,我找到了这个 BUG 链接:

https://bugs.openjdk.org/browse/JDK-6806875

这两个 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”,然而具体让我形容进去我也有点说不出来。

说人话就是:我懂一点,然而不多。

于是又通过一番查找,我找到了这个链接,在这外面彻底搞明确是怎么一回事了:

http://concurrencyfreaks.blogspot.com/2016/10/self-linking-an…

在这个链接外面提到了一个视频,它让我从第 23 分钟开始看:

我看了一下这个视频,应该是 2015 年公布的。因为整个会议的主题是:20 years of Java, just the beginning:

https://www.infoq.com/presentations/twitter-services/

这个视频的主题是叫做“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 这个办法:

然而过后我齐全没有思考到文本提到的问题,顺着代码就捋过来了。

我感觉看到这部分代码,而后能提出本文中这两个问题的人,才是在带着本人思考深度浏览源码的人。

解决问题不厉害,提出问题才是最屌的,因为当一个问题提出来的时候,它就曾经被解决了。

带着质疑的眼光看代码,带着求真的态度去摸索,与君共勉之。

正文完
 0