在知乎上看到一個問題《java中volatile關鍵字的疑惑?》,引發了個人興趣html
問題是這樣的:java
1 package com.cc.test.volatileTest; 2 3 public class VolatileBarrierExample { 4 private static boolean stop = false; 5 6 public static void main(String[] args) throws InterruptedException { 7 Thread thread = new Thread(new Runnable() { 8 @Override 9 public void run() { 10 while (!stop) { 11 } 12 } 13 }); 14 15 thread.start(); 16 Thread.sleep(1000); 17 stop = true; 18 thread.join(); 19 } 20 }
這段代碼的主要目的是:主線程修改非volatile類型的全局變量stop,子線程輪詢stop,若是stop發生變更,則程序退出。編程
可是若是實際運行這段代碼會形成死循環,程序沒法正常退出。windows
若是對Java併發編程有必定的基礎,應該已經知道這個現象是因爲stop變量不是volatile的,主線程對stop的修改不必定能被子線程看到而引發的。架構
可是題主玩了個花樣,額外定義了一個static類型的volatile變量i,在while循環中對i進行自增操做,代碼以下所示:併發
1 package com.cc.test.volatileTest; 2 3 public class VolatileBarrierExample { 4 private static boolean stop = false; 5 private static volatile int i = 0; 6 7 public static void main(String[] args) throws InterruptedException { 8 Thread thread = new Thread(new Runnable() { 9 @Override 10 public void run() { 11 int i = 0; 12 while (!stop) { 13 i++; 14 } 15 } 16 }); 17 18 thread.start(); 19 Thread.sleep(1000); 20 stop = true; 21 thread.join(); 22 } 23 }
這段程序是能夠在運行一秒後結束的,也就是說子線程對volatile類型變量i的讀寫,使非volatile類型變量stop的修改對於子線程是可見的!app
看起來使人感到困惑,可是實際上這個問題是不成立的。ide
先給出歸納性的答案:stop變量的可見性不管在哪一種場景中都沒有獲得保證。這兩個場景中程序是否能正常退出,跟JVM實現與CPU架構有關,沒有肯定性的答案。函數
下面從兩個不一樣的角度來分析優化
第一個場景就不談了,即便在第二種場景裏,雖然子線程中有對volatile類型變量i的讀寫+非volatile類型變量stop的讀,可是主線程中只有對非volatile類型變量stop的寫入,所以沒法創建 (主線程對stop的寫) happens-before於 (子線程對stop的讀) 的關係。
也就是不能期望主線程對stop的寫必定能被子線程看到。
雖然場景二在實際運行時程序依然正確終止了,可是這個只能算是運氣好,若是換一種JVM實現或者換一種CPU架構,可能場景二也會陷入死循環。
能夠設想這樣的一個場景,主/子線程分別在core1/core2上運行,core1的cache中有stop的副本,core2的cache中有stop與i的副本,並且stop和i不在同一條cacheline裏。
core1修改了stop變量,可是因爲stop不是volatile的,這個改動能夠只發生在core1的cache裏,而被修改的cacheline理論上能夠永遠不刷回內存,這樣core2上的子線程就永遠也看不到stop的變化了。
因爲run方法裏的while循環會被執行不少次,因此必然會觸發jit編譯,下面來分析兩種狀況下jit編譯後的結果(觸發了屢次jit編譯,只貼出最後一次C2等級jit編譯後的結果)
如何查看JIT後的彙編碼請參看個人這篇博文:《如何在windows平臺下使用hsdis與jitwatch查看JIT後的彙編碼》
ps. 回答首發於知乎,從新截圖太麻煩,所以實際分析使用的Java源碼與前面貼的代碼略有不一樣,不影響理解,會意便可。
若是把jit編譯後的代碼改寫回來,大概是這個樣子
1 if(!stop){ 2 while(true){ 3 i++; 4 } 5 }
很是明顯的指令重排序,JVM以爲每次循環都去訪問非volatile類型的stop變量太浪費了,就只在函數執行之初訪問一次stop,後續不管stop變量怎麼變,都無論了。
第一種狀況死循環就是這麼來的。
從第一個紅框開始看:
也就是說,每次循環都會去訪問一次stop變量,最終訪問到stop被修改後的新值(可是不能確保在全部JVM與全部CPU架構上都必定能訪問到),致使循環結束。
這兩種場景的區別主要在於第二種狀況的循環中有對static volatile類型變量i的訪問,致使jit編譯時JVM沒法作出激進的優化,是附加的效果。
涉及到內存可見性的問題,必定要用happens-before原則細緻分析。由於你很難知道JVM在背後悄悄作了什麼奇怪的優化。