why技术

分享一个我遇到过的“量子力学”级别的BUG。

你好呀,我是歪歪。

前几天在网上冲浪的时候,看到知乎上的这个话题:

Image

一瞬间,一次历史悠久但是记忆深刻的代码调试经历,“刷”的一下,就在我的脑海中蹦出来了。

虽然最终定位到的原因令人无语,对于日常编码也没啥帮助,但是真的是:

Image

情景再现

我记得当时我是学习 ConcurrentLinkedQueue (下文用 CLQ  代替)这个玩意,为了比较深入的掌握这个玩意,我肯定是要 Debug 跟踪一下源码的。

问题就出现在 Debug 的时候,现象非常诡异,听我细细道来。

首先,我当时的 Demo 极其简单,就这么两行代码:

Image

new 一个 CLQ 对象,然后调用 offer 方法塞一个对象进去。

完事了。

这么简单的代码能搞出什么牛逼的玩意呢?

首先,我带你看看 CLQ 的数据结构。

CLQ 是由一个个 Node 组成的链式结构。

Image

new CLQ 的时候通过 new Node() 构造出一个特殊的“dummy node”,翻译过来大家一般叫它“哑元节点”。

然后将头指针 head 和尾指针 tail 都指向这个哑元节点。

那这个 Node 长啥样呢?

Node 里面有一个 item(放的是存储的对象),还有一个 next 节点(指向的是当前 Node 的下一个节点):

Image

从数据结构来看,也知道这是一个单向链表了。

当时为了学它,我想通过日志的方式直接输出链表结构,这应该是最简单的演示方式了。

毕竟 Java 程序员,就靠日志活着了。

所以我当时自定义了一个 WhyConcurrentLinkedQueue(下文简写为 WhyCLQ)。

这个 WhyCLQ 是怎么来的呢?

非常简单,我直接把 JDK 源码中的 CLQ 复制出来一份,改名为 WhyCLQ 就完事了。

Image

然后搞个测试用例跑跑:

Image

非常 nice,没有任何毛病。

我们现在可以任意的在代码中增加输出日志了。

比如,我想要看 WhyCLQ 这个链式结构到底是怎么样的。

我们可以在自定义的 CLQ 里面加一个打印链表结构的方法:

public void printWhyCLQ() {
    StringBuilder sb = new StringBuilder();
    for (Node<E> p = first(); p != null; p = succ(p)) {
        E item = p.item;
        sb.append(item).append("->");
    }
    System.out.println("链表item对象指向 =" + sb);
}

然后在每次 offer 方法新增完成后,调用一下 printWhyCLQ 方法,输出当前的链式结构:

Image

其他的地方类似,只要你觉得源码看起来有点绕的地方,你就可以加输出语句,哪怕一行代码就配上一行输出语句也没问题。

甚至,你还能“客制化”源码,但是这不是本文的重点,我就不展开了。

通过复制源码的方式自定义一个 JDK 源码中的类,然后加上大量的输出语句,有时候也会对源码进行各种改装,是我常用的一个学习小技巧,分享给你,不用客气。

当你被一步步 debug 带晕的时候,你可以试一试这种方式,先整体再局部。

好,到这里就算是铺垫完成了。

我们回到最开始的这两行代码:

Image

按照我们的理解,第一次 offer 之后,对应的链表画个简图应该是这样的:

Image

但是最后的输出是这样的:

Image

为什么输出的日志不是 null->@4629104a 呢?

因为我们自定义的 printWhyCLQ 这个方法里面会调用 first 方法,获取真正的头节点,即 item 不为 null 的节点:

Image

也就是我框起来的地方:first 方法中的 updateHead(h, p) 方法,会去修改头结点。

然后,我还想在第一次 offer 的时候,详细的输出头结点的信息,所以加了这几行输出语句:

Image

直接把程序跑起来,对应的效果是这样的:

Image

但是,当我在这个分支入口,打上断点,用 debug 模式进行调试的时候:

Image

运行结果是这样的:

Image

空指针异常!!!???

为了让你有更加直观的感受,我给你上个动图。

首先,是直接把程序运行起来的动图:

Image

这是 Debug 运行时的动图:

Image

如果前面的文字你没看懂,不重要,你只需要记住下面这个现象:

同样的程序,当你直接运行,就能正常结束,当你用 Debug 模式运行的时候,就会抛出空指针异常。

来,如果是你遇到这个问题,你会怎么办?

当年我还是一个萌新菜鸟的时候,遇到这个问题,直接就懵逼了啊,百思不得其解,感觉编程的大厦正在摇摇欲坠。

这真的就很诡异啊!

Image

当你直接运行程序,会拿到一个预期的结果。

但是试图通过 Debug 模式去观察这个程序的时候,这个程序就会抛出异常。

这很难不让人想起“量子力学”中的光的双缝干涉试验啊。

观测手段触发了光的粒子状态,所以没有干涉条纹。

如果不观测,光就是波的形态,出现了干涉条纹。

如果你不知道我在说什么,一点也不重要。

但是你知道我在说什么,你就知道,歪师傅这个程序的现象,用“量子力学”来形容是多么的贴切。

我甚至还怀疑过是质子,一定是质子在搞事情。

Image

当时,我是怎么解决这个问题的呢?

没有解决。

当年经验浅薄,现象又太过诡异导致我不知道应该怎么去解决,而且最重要的是并没有影响我理解 CLQ 这个玩意。

是的,感谢我当时还记得主要目标是去学习 CLQ,而不是去研究这个诡异的现象。

偶遇真相

我忘了隔了多长时间,只记得是一个麦子黄了的季节,我在这个链接中偶遇到了真相:

https://stackoverflow.com/questions/55889152/why-my-object-has-been-changed-by-intellij-ideas-debugger-soundlessly

Image

这个哥们遇到的问题和我一模一样,但是这个问题下面只有一个回答:

Image

这个回答给出的解决方案

最后的解决方案就是关闭 IDEA 的这两个配置,他们默认是开启的:

Image

当关闭这两个配置后,我的程序在 Debug 的时候也正常了。

为什么呢?

因为 IDEA 在 Debug 模式下会主动的帮我们调用一次 toString 方法。

而在 CLQ 的 toString 方法里面,会去调用 first 方法:

Image

前面我说了:first 方法中的 updateHead(h, p) 方法,会去修改头结点。

Image

之前我给的简图是这样的:

Image

由于 Debug 会调用 toString 方法,从而触发了 first 方法,进而导致了头结点不是 null,而是这个 obj 了:

Image

再到 this.head.next 这里获取头结点的 next 的时候,由于 next 并不存在,值为 null:

Image

所以 this.head.next.item 抛出了空指针异常。

没有什么玄学,我们要相信科学。

但是,这个真相确实有点坑。

Image

IDEA 图啥?

那么问题就来了。

为什么 IDEA 要在 Debug 的时候默认调用一下 toString 方法呢?

我用 HashMap 举例,给你上个对比图你就知道它想要干啥了。

这是默认配置的情况:

Image

可以直观的看到 map 中 key 和 value 的情况

当我们取消前面说的配置:

Image

再次 Debug 的时候,看到的就是这样的:

Image

而且可以看到,toString 方法是可以点击的。

当你点击之后,就变成了这样:

Image

这么一对比,就很直观了。

你说 IDEA 图啥?

还不就说图用户调试起来的时候,看起来更加直观嘛,确实是一片好心。

谁能想到你 toString 方法中还能藏着一些逻辑呢。

这波我站 IDEA。

又学到一个埋坑的小技巧

通过前面的介绍,我仿佛又掌握了一个埋坑的小技巧。

我给你演示一下。

首先我定义一个 why 的类:

Image

这个类的 toSting 方法中有 age++ 这样的操作。

当你直接运行这个程序的时候,运行结果为 18:

Image

但是,当你 Debug 的时候:

Image

age 就变成 19 了。

而且是看一次,就涨一岁,这你受得了吗:

Image

如果代码再复杂一点,找问题都让你焦头烂额了。

谁能想到 IDEA 在你 Debug 的时候帮你调用了 toString,谁又能想到 toString 方法中还有逻辑呢?

如果 toString 方法中的逻辑,和前面说的 CLQ 一样,会影响到你要寻找的答案...

这一套丝滑小连招下来,你就玩去吧。

一个埋坑的小技巧,没到血海深仇,不要轻易使用。

最后,你说上帝在编程的时候,会不会也是埋了这样的一个坑。

当我们直接运行“光”这个方法的时候,光就是波的形态。

但是当我们使用通过观察手段去 Debug “光”这个方法到底是怎么运行的时候,上帝他老人家就会在“光的 toStirng 方法”中主动调用一个让光变成粒子的逻辑。

所以,我们的任何观测手段都会触发这个“光的 toStirng 方法”,导致光的出现了粒子状态,在光的双缝干涉试验直接中,就没有出现干涉条纹。

从编程角度,看量子力学,我就问你溜不溜。

Image

好了,本文的技术部分就到这里啦。

下面这个环节叫做[荒腔走板],技术文章后面我偶尔会记录、分享点生活相关的事情,和技术毫无关系。我知道看起来很突兀,但是我喜欢,因为这是一个普通博主的生活气息

荒腔走板

Image

既然都出现量子力学了,那给大家推荐一本书:《上帝掷骰子吗?》

我记得我刚开始看这本书的时候是一个周六的深夜,白天的时候我还在读另外一本书,吃饭的间隙我随手翻看了几页,导致我后面看另外一本书的最后几节时候,心不在焉,心里老是想着两朵乌云,量子力学,多重宇宙,双缝干涉……我从周六的深夜读到了第二天的凌晨 2 点半,早上 9 点又爬起来接着看。

这本书的开篇就是这么引人入胜,至少对我来说是这样的。

作者就是我在量子世界的引导者,把我少的可怜的量子知识储备串联了起来。

在这之前,我仅仅知道双缝干涉实验,光的波粒二象性,以太介质,测不准定律,薛定谔的猫,多重宇宙,量子纠缠这些词语,并且有些只是大概知道,有些只是知道词语,说不清楚概念。

书的开篇,是一部武侠世界,看到中间的时候,是一本描述神仙打架的"修仙网文",最后,他是一本哲学书,甚至是一本神学书。

我也开始幻想,当我们真的探索到我们认为的宇宙的终极奥秘的时候,对全人类宣布,物理的大厦已经基本落成,接下来就是对其进行简单的修饰的时候,还会不会有几朵小乌云正在悄悄聚集。

整本书除了后面哲学的部分,有点晦涩难懂,其他部分可以看的酣畅淋漓。

推荐给你,量子力学的诱惑,真的很棒。

··············  END  ··············

图片

你好呀,我是歪歪。我没进过一线大厂,没创过业,也没写过书,更不是技术专家,所以也没有什么亮眼的title。

当年高考,随缘调剂到了某二本院校计算机专业。纯属误打误撞,进入程序员的行列,之后开始了运气爆棚的程序员之路。

说起程序员之路还是有点意思,可以点击蓝字,查看我的程序员之路。