我通过调试ConcurrentLinkedQueue发现一个IDEA的小虫子(bug), vscode复现, eclipse毫无问题

Posted bangiao

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了我通过调试ConcurrentLinkedQueue发现一个IDEA的小虫子(bug), vscode复现, eclipse毫无问题相关的知识,希望对你有一定的参考价值。

前言: 本渣渣想分析分析Doug Lea大佬对高并发代码编写思路, 于是找到了我们今天的小主角ConcurrentLinkedQueue进行鞭打, 说实话草稿我都打好了, 就差临门一脚, 给踢折了

技术图片

直接看问题, ideaDebug非Debug模式下运行结果不同, vscode复现, eclipse毫无鸭梨

怎么发现的问题?

从这段代码开始

public static void main(String[] args) throws InterruptedException, NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException {
	ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
	queue.add("zhazha");
	// 在下面这行下断点
	Field headField = queue.getClass().getDeclaredField("head");
	headField.setAccessible(true);
	Object head = headField.get(queue);
	
	Field itemField = queue.getClass().getDeclaredField("ITEM");
	itemField.setAccessible(true);
	VarHandle ITEM = (VarHandle) itemField.get(head);
	Object o = ITEM.get(head);
	System.out.println(o);
}

你会发现一个神奇的现象, 如果我们下断点在Field headField = queue.getClass().getDeclaredField("head");这一行代码, 单步执行下来会发现System.out.println(o);打印出了zhazha, 但是如果不下断点, 直接运行打印null

为了防止是WARNING: An illegal reflective access operation has occurred警告的影响, 我改了改源码, 用unsafe获取试试

private static Unsafe unsafe;

static {
	Class<Unsafe> unsafeClass = Unsafe.class;
	Unsafe unsafe = null;
	try {
		Field unsafeField = unsafeClass.getDeclaredField("theUnsafe");
		unsafeField.setAccessible(true);
		ConcurrentLinkedQueueDemo.unsafe = (Unsafe) unsafeField.get(null);
	} catch (NoSuchFieldException | IllegalAccessException e) {
		e.printStackTrace();
	}
}

public static void main(String[] args) throws InterruptedException, NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException {
	ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
	queue.add("zhazha");
	// 在下面这行下断点
	long headOffset = unsafe.objectFieldOffset(queue.getClass().getDeclaredField("head"));
	Object head = unsafe.getObject(queue, headOffset);
	
	long itemOffset = unsafe.staticFieldOffset(ConcurrentLinkedQueue.class.getDeclaredField("ITEM"));
	Object base = unsafe.staticFieldBase(ConcurrentLinkedQueue.class.getDeclaredField("ITEM"));
	VarHandle ITEM = (VarHandle) unsafe.getObject(base, itemOffset);
	
	Object o = ITEM.get(head);
	System.out.println(o);
}

完美复现

第一反应我的问题

去源码里看看怎么回事. 但.......这...........

技术图片

仔细看红箭头的地址, tpheadtail都是同一个地址, 看上面的代码发现全是tail赋值给这三个变量的
NEXT源码

技术图片

他的接收类是Node, 接收字段是next, 接收字段类型Node

看这源码的势头, NEXT修改的是p对象, 如果该对象的next节点为null, 则把newNode设置到节点上, 此时p对象指向的是tail, 同时head也是指向的tail节点, 所以这句话执行完毕, head.nexttail.next同样都是newNode节点
但.....................这.....................

技术图片

head节点被直接替换掉, tail保持不变

此时我的表情应该是这样

技术图片

怀疑猫生

private static Unsafe unsafe;

static {
	Class<Unsafe> unsafeClass = Unsafe.class;
	Unsafe unsafe = null;
	try {
		Field unsafeField = unsafeClass.getDeclaredField("theUnsafe");
		unsafeField.setAccessible(true);
		ConcurrentLinkedQueueDemo.unsafe = (Unsafe) unsafeField.get(null);
	} catch (NoSuchFieldException | IllegalAccessException e) {
		e.printStackTrace();
	}
}

public static void main(String[] args) throws InterruptedException, NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, InstantiationException {
	ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
	queue.add("zhazha");
	
	// 在这里下断点
	Class<? extends ConcurrentLinkedQueue> queueClass = queue.getClass();
	
	Object head = unsafe.getObject(queue, unsafe.objectFieldOffset(queueClass.getDeclaredField("head")));
	
	Field itemField = queueClass.getDeclaredField("ITEM");
	itemField.setAccessible(true);
	VarHandle ITEM = (VarHandle) itemField.get(queue);
	Object item = ITEM.get(head);
	System.out.println(item); // zhazha
	
	
	long itemOffset = unsafe.staticFieldOffset(queueClass.getDeclaredField("ITEM"));
	Object base = unsafe.staticFieldBase(queueClass.getDeclaredField("ITEM"));
	VarHandle ITEM2 = (VarHandle) unsafe.getObject(base, itemOffset);
	Object item2 = ITEM2.get(head);
	System.out.println(item2); // zhazha
}

单步调试出来还是zhazha, 而且为了防止反射出了问题, 我同时用了Unsafe和反射两种方法

技术图片

copy 源码添加自己的调试函数再次测试

得了得了, 放终极大招试试, copy ConcurrentLinkedQueue源码出来改成MyConcurrentLinkedQueue
offer方法添加几个输出

public boolean offer(E e) {
	final Node<E> newNode = new Node<E>(Objects.requireNonNull(e));
	
	for (Node<E> t = tail, p = t; ; ) {
		Node<E> q = p.next;
		if (q == null) {
			if (NEXT.compareAndSet(p, null, newNode)) {
				System.out.println("this.head.item = " + this.head.item);
				System.out.println("this.tail.item = " + this.tail.item);
				System.out.println("this.head.next.item = " + this.head.next.item);
				System.out.println("this.tail.next.item = " + this.tail.next.item);
				if (p != t) {
					TAIL.weakCompareAndSet(this, t, newNode);
				}
				return true;
			}
		}
		else if (p == q) {
			p = (t != (t = tail)) ? t : head;
		}
		else {
			p = (p != t && t != (t = tail)) ? t : q;
		}
	}
}

主函数就比较简单了直接

public static void main(String[] args) {
	MyConcurrentLinkedQueue<String> queue = new MyConcurrentLinkedQueue<String>();
	queue.add("zhazha");
}

直接在非Debug模式下运行, 发现打印出来的是

this.head.item = null
this.tail.item = null
this.head.next.item = zhazha
this.tail.next.item = zhazha

Process finished with exit code 0

Debug模式下单步运行发现

this.head.item = zhazha
this.tail.item = null
Exception in thread "main" java.lang.NullPointerException
	at com.zhazha.juc.MyConcurrentLinkedQueue.offer(MyConcurrentLinkedQueue.java:117)
	at com.zhazha.juc.MyConcurrentLinkedQueue.add(MyConcurrentLinkedQueue.java:67)
	at com.zhazha.juc.MyConcurrentLinkedQueueDemo.main(MyConcurrentLinkedQueueDemo.java:13)
Process finished with exit code 1

纳尼?

不信邪的我在NEXT cas操作的前后增加了sleep方法, 以非Debug模式下运行

技术图片

this.head.item = null
this.tail.item = null
this.head.next.item = zhazha
this.tail.next.item = zhazha

还是不一样

多环境IDE测试

放终极终极终极SVIP大招 ===> 放在eclipse上试试??? 或者vscode上???

在vscode上以Debug模式单步运行输出

this.head.item = zhazha
this.tail.item = null
Exception in thread "main" java.lang.NullPointerException
        at MyConcurrentLinkedQueue.offer(MyConcurrentLinkedQueue.java:116)
        at MyConcurrentLinkedQueue.add(MyConcurrentLinkedQueue.java:66)
        at MyConcurrentLinkedQueueDemo.main(MyConcurrentLinkedQueueDemo.java:11)

技术图片

非Debug模式直接输出

this.head.item = null
this.tail.item = null
this.head.next.item = zhazha
this.tail.next.item = zhazha

在eclipse上以Debug模式单步运行输出

this.head.item = null
this.tail.item = null
this.head.next.item = zhazha
this.tail.next.item = zhazha

技术图片

非Debug运行输出

this.head.item = null
this.tail.item = null
this.head.next.item = zhazha
this.tail.next.item = zhazha

发现了没有? 还是我大eclipse坚挺住了

技术图片




以上是关于我通过调试ConcurrentLinkedQueue发现一个IDEA的小虫子(bug), vscode复现, eclipse毫无问题的主要内容,如果未能解决你的问题,请参考以下文章

(Android Studio)如何仅通过我的代码进行调试?

通过 GDB 调试 DMD 生成程序

是否可以在调试版本中编译 VS 项目,包括要通过不同项目调试的断点?

VSCode 新手:通过 Docker 进行远程 Jest/Node 调试

如何将 NodeJS 调试器通过隧道传送到 Vagrant 框?

如何通过chrome远程调试获取网页资源内容