背景
最近, 在复习 JUC 的时候调试了一把 ConcurrentLinkedQueue 的 offer 方法, 意外的发现 Idea 在 debug 模式下竟然会 "自动修改" 已经创建的 Java 对象, 当时觉得这个现象很是奇怪, 现在把问题的原因以及解决过程记录下来, 希望你在调试的时候不要踩坑.
调试代码
调试的代码很简单, 就是多次调用 offer 方法, 然后观察 ConcurrentLinkedQueue 的 head 和 tail 属性.
- import java.lang.reflect.Field;
- import java.util.concurrent.ConcurrentLinkedQueue;
- public class ConcurrentLinkedQueueTest {
- public static void main(String[] args) {
- ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
- print(queue);
- queue.offer("aaa");
- print(queue);
- queue.offer("bbb");
- print(queue);
- queue.offer("ccc");
- print(queue);
- }
- /**
- * 打印并发队列 head 属性的 identityHashCode
- * @param queue
- */
- private static void print(ConcurrentLinkedQueue queue) {
- Field field = null;
- boolean isAccessible = false;
- try {
- field = ConcurrentLinkedQueue.class.getDeclaredField("head");
- isAccessible = field.isAccessible();
- if (!isAccessible) {
- field.setAccessible(true);
- }
- System.out.println("head:" + System.identityHashCode(field.get(queue)));
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- field.setAccessible(isAccessible);
- }
- }
- }
调试过程
上述代码在 Idea 中 debug 模式下 head 属性会无缘无故的被修改(run 模式下正常, debug 模式下关闭所有断点也正常), 检查 ConcurrentLinkedQueue 的源码发现, head 属性只有在构造器和反序列化的 readObject 共 3 处地方才会被直接赋值(不是 cas 修改), 我也仔细检查了 offer 方法, 确实没有修改 head 的地方. 而 Idea 在 debug 时, 把 head 属性修改为第一次 offer 的 Node 节点, 这个现象就很奇怪了.
在 run 模式下的输出结果, 多次调用 offer 方法, head 属性都是同一个对象(debug 模式下关闭所有断点也是同样的效果)
在 offer 方法中断点, 然后 debug 并单步调试(Step over)
在 offer 方法中断点, 然后 debug 并直接运行到下一个断点(Resume program)
由上可见, 在 debug 进入 offer 方法之后 head 属性确实被修改了(对象已经不是同一个), 而且这不是偶尔出现, 而是一直可以复现的, Step over 和 Resume program 也表现出了修改 head 属性不同的时机, 这让人很费解. 更费解的是就算不在 offer 方法体里断点, 在 main 方法中断点也会出现 head 被修改的现象.
转战到 Eclipse, 同样的环境, 同样的操作, 在 run 和 debug 模式下都不会出现 head 被修改的情况
分析
了解到 Idea 在 debug 模式下默认开启了 toString 预览特性 (Settings>>Build,Execution,Deployment>>Debugger>>Data Views>>Java>>Enable 'toString()' object view), 可是调用 toString 方法也不至于把对象本身都修改了啊, 也专门看了下 ConcurrentLinkedQueue 的内部类 Node, 并没有复写 toString 方法(事后回顾, 当时在这里疏忽了, 下文会再介绍), 但还是关掉特性再测试一遍, 然而还是同样的结果, head 属性任然被悄悄的修改了. 第二天来到公司在同事的环境(IntelliJ IDEA 2019.1) 上验证了下, 还是同样的问题, 排除 Idea 版本的因素.
郁闷了一会儿, 就向 "网友" 提问了链接, 不久就得到了 IntelliJ IDEA 的产品经理 yole 的回复, 他的意思还是 Idea 的 Data Views 的 toString 在作怪, 上文已经说过关掉 toString 特性还是有这个问题, 但是他给了我一个重要的思路就是: 在 debug 模式下, ConcurrentLinkedQueue 的对象也会被调用 toString 方法的, 在队列的 toString 方法中会获取队列的迭代器, 而创建迭代器时会调用 first 方法, first 方法里就会 cas 修改 head 属性.(之前确实没考虑到队列本身的 toString 方法, 而是去看 Node 是否重写了 toString, 手动哭脸)
这里需要注意的是, 尽管关掉 toString 特性上面问题还是存在, 原因就在于 ConcurrentLinkedQueue 是一个 Collection,Data Views 还有一个选项 "Enable alternative view for Collections classes" (平时没注意...)所以也会在 debug 时造成队列迭代器的遍历. 把这个特性也一并关掉, 则上面的问题就不会再出现了.
总结
之前看到有网友在调试低版本的 fastjson 的反序列化时也遇到过 Idea toString 的问题, 虽然这可能不会影响程序的正常执行, 但是作为开发人员, 在 debug 时完全可能会遇到这种被 Idea 挖坑的情况. 对于 Data views 特性我目前也持保留态度, 如果不是特别依赖就直接关掉吧.
参考:
https://www.cnblogs.com/oldtrafford/p/8612089.html
来源: https://www.cnblogs.com/ocean234/p/10779784.html