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

news/2025/3/17 20:01:35/文章来源:https://www.cnblogs.com/thisiswhy/p/18777464

你好呀,我是歪歪。

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

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

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

情景再现

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

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

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

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

完事了。

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

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

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

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

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

那这个 Node 长啥样呢?

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

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

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

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

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

这个 WhyCLQ 是怎么来的呢?

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

然后搞个测试用例跑跑:

非常 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 方法,输出当前的链式结构:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

运行结果是这样的:

空指针异常!!!???

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

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

这是 Debug 运行时的动图:

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

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

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

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

这真的就很诡异啊!

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

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

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

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

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

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

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

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

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

没有解决。

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

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

偶遇真相

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

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

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

这个回答给出的解决方案

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

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

为什么呢?

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

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

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

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

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

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

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

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

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

IDEA 图啥?

那么问题就来了。

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

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

这是默认配置的情况:

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

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

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

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

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

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

你说 IDEA 图啥?

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

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

这波我站 IDEA。

又学到一个埋坑的小技巧

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

我给你演示一下。

首先我定义一个 why 的类:

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

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

但是,当你 Debug 的时候:

age 就变成 19 了。

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

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

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

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

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

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

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

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

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

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

从编程角度,看量子力学,有点意思。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/900539.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

day:23 python模块——时间,random,string

一、模块的介绍 (1)python模块,是一个python文件,以一个.py文件,包含了python对象定义和pyhton语句(2)python对象定义和python语句 (3)模块让你能够有逻辑地组织你的python代码段。 (4)把相关的代码分配到一个模块里能让你的代码更好用,更易懂 (5)模块能定义函数…

实验一:Tableau数据可视化入门

实验目的:1.熟悉TableauDesktop使用方法。2.通过Tableau软件来实现Excel中数据的基本可视化。 实验原理: Tableau是新一代商业智能工具软件,它将数据连接、运算、分析与图表结合在 一起,通过拖放方式创建各种图表。 Tableau产品包括TableauDesktop、Tableauserver、Tableau…

实验二:D3数据可视化基础

实验目的:熟悉 D3 数据可视化的使用方法。 实验原理:D3 的全称是(Data-Driven Documents),是一个被数据驱动的文档,其实就是 一个 JavaScript 的函数库,使用它主要是用来做数据可视化的。本次实践主要介绍D3一些最基本的使用方法,以及生成一些比较简单的图表。D3 是一个…

PCB的通孔、盲孔、埋孔|元器件的符号和封装

他们的本质都是用来切换的层的通孔:从顶层到底层,可以看到头 盲孔:看不到头的,如图从第一层切换到了第二层 埋孔:顾名思义就是埋进去了,无论从正面还是反面都是看不到的,它是处于内层的原理图就是一个表示符号,封装是元器件具体实物大小,具体形状

在IDEA编辑器中,如何在.gitignore 的文件中,把 .ides 的文件忽略,提交git的时候不提交 .idea文件夹

方法 1:直接编辑 .gitignore 文件创建或编辑 .gitignore 文件在项目根目录(与 .git 文件夹同级)右键点击 → New → File,输入文件名 .gitignore。如果已存在 .gitignore,直接双击打开。添加忽略规则在 .gitignore 文件中添加以下内容:# 忽略所有 .idea 目录及其内容 .id…

揭秘EtherCAT转profinet玻璃制造厂的复杂生产环境与智能设备运用

玻璃制造厂的生产环境都比较复杂,需要严格的操作规程,及安全规范。玻璃制造厂的生产环境通常具有以下特点:高温环境:玻璃的熔化过程需要在高温下进行,熔炉的温度通常达到1400℃以上。因此,厂房内的设备和材料必须能够耐高温,并具备良好的隔热性能。 粉尘和化学物质:在玻…

20242943 2024-2025-2 《网络攻防实践》实验三

一.实验内容(1)动手实践了tcpdump等嗅探工具。通过嗅探工具,可以分析进入某一网站时,浏览器访问了多少个web服务器以及它们的IP地址都是什么。(2)动手实践Wireshark等抓包工具。通过使用Wireshark开源软件对在本机上以TELNET方式登录BBS进行嗅探与协议分析,得出了所登录…

20241904 2024-2025-2 《网络攻防实践》实验三

一、.实验内容动手实践tcpdump 使用tcpdump开源软件对在本机上访问www.tianya.cn网站过程进行嗅探,回答问题:在访问www.tianya.cn网站首页时,浏览器将访问多少个Web服务器? 他们的IP地址都是什么?动手实践Wireshark 使用Wireshark开源软件对在本机上以TELNET方式登录BBS进…

context的应用

1. 简介 在 Go 语言中,context 包主要用于在 并发编程 中控制和管理 goroutine 的生命周期。它提供了一种机制,可以通过传递 context.Context 来协调多个 goroutine,特别是在需要取消操作、超时控制和传递共享数据时。 2. 常见用法2.1 控制goroutine的生命周期(cancel) co…

Semantic Kernel:Phi-4 mini的tools

Phi4-mini开始支持tools了,但在第一时间试用时不理想,kenfey最近发了一篇解决方案,详见https://techcommunity.microsoft.com/blog/educatordeveloperblog/building-ai-agents-on-edge-devices-using-ollama--phi-4-mini-function-calling/4391029。本篇文章想更详细地梳理一…

Creo 11下载与安装教程

Creo是一款由PTC(Parametric Technology Corporation)开发的计算机辅助设计(CAD)软件套件。Creo包括多个模块,用于实现产品设计、建模、分析和制造过程中的各个环节。其中,Creo Parametric是其核心组件,提供了参数化建模功能,可以创建几何形状并应用关联参数,使设计过…

局域网下Python oracledb 连接远程服务器 Oracle 11g

Windows 里ipconfig 一下查服务器的IP 我的地址是在这里,F:\app\dell\product\11.2.0\dbhome_1\NETWORK\ADMIN 默认一般是在这里,C:\oracle\product\11.2.0\dbhome_1\network\admin\listener.ora# listener.ora Network Configuration File: F:\app\dell\product\11.2.0\db…