1. 程式人生 > 實用技巧 >我通過除錯ConcurrentLinkedQueue發現一個IDEA的小蟲子(bug), vscode復現, eclipse毫無問題

我通過除錯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堅挺住了