一個 println 居然比 volatile 還好使?,跪了

前兩天一個小夥伴忽然找我求助,說準備換個坑,最近在系統學習多線程知識,但遇到了一個刷新認知的問題……java

做爲閱讀福利,小編也整理一些Java筆記資料(包含面試真題+腦圖+手寫pdf)須要的自行領取~git

最全學習筆記大廠真題+微服務+MySQL+分佈式+SSM框架+Java+Redis+數據結構與算法+網絡+Linux+Spring全家桶+JVM+高併發+各大學習思惟腦圖+面試集合面試

在這裏插入圖片描述

在這裏插入圖片描述

小夥伴:Effective JAVA 裏的併發章節裏,有一段關於可見性的描述。下面這段代碼會出現死循環,這個我能理解,JMM 內存模型嘛,JMM 不保證 stopRequested 的修改能被及時的觀測到。算法

static boolean stopRequested = false;



public static void main(String[] args) throws InterruptedException {



    Thread backgroundThread = new Thread(() -> {

        int i = 0;

        while (!stopRequested) {

            i++;

        }

    }) ;

    backgroundThread.start();

    TimeUnit.MICROSECONDS.sleep(10);

    stopRequested = true ;

}

但奇怪的是在我加了一行打印以後,就不會出現死循環了!難道我一行 println 能比 volatile 還好使啊?這倆也不要緊啊express

static boolean stopRequested = false;



public static void main(String[] args) throws InterruptedException {



    Thread backgroundThread = new Thread(() -> {

        int i = 0;

        while (!stopRequested) {



            // 加上一行打印,循環就能退出了!

        	System.out.println(i++);

        }

    }) ;

    backgroundThread.start();

    TimeUnit.MICROSECONDS.sleep(10);

    stopRequested = true ;

}

我:小夥子八股文背的挺熟啊,JMM 張口就來。 ?網絡

我:這個……實際上是 JIT 乾的好事,致使你的循環沒法退出。JMM 只是一個邏輯上的內存模型,內部有些機制是和 JIT 有關的數據結構

好比你第一個例子裏,你用-Xint禁用 JIT,就能夠退出死循環了,不信你試試?多線程

小夥伴:臥槽,真的能夠,加上 -Xint 循環就退出了,好神奇!JIT 是個啥啊?還能有這種功效?併發

JIT(Just-in-Time) 的優化框架


衆所周知,JAVA 爲了實現跨平臺,增長了一層 JVM,不一樣平臺的 JVM 負責解釋執行字節碼文件。雖然有一層解釋會影響效率,但好處是跨平臺,字節碼文件是平臺無關的。

在 JAVA 1.2 以後,增長了 即時編譯(Just-in-Time Compilation,簡稱 JIT) 的機制,在運行時能夠將執行次數較多的熱點代碼編譯爲機器碼,這樣就不須要 JVM 再解釋一遍了,能夠直接執行,增長運行效率。

?

但 JIT 編譯器在編譯字節碼時,可不只僅是簡單的直接將字節碼翻譯成機器碼,它在編譯的同時還會作不少優化,好比循環展開、方法內聯等等…… ?

這個問題出現的緣由,就是由於 JIT 編譯器的優化技術之一 - 表達式提高(expression hoisting) 致使的。

表達式提高(expression hoisting)

先來看個例子,在這個 hoisting 方法中,for 循環裏每次都會定義一個變量 y,而後經過將 x*y 的結果存儲在一個 result 變量中,而後使用這個變量進行各類操做

public void hoisting(int x) {

	for (int i = 0; i < 1000; i = i + 1) {

		// 循環不變的計算 

		int y = 654;

		int result = x * y;



		// ...... 基於這個 result 變量的各類操做

	}

}

可是這個例子裏,result 的結果是固定的,並不會跟着循環而更新。因此徹底能夠將 result 的計算提取到循環以外,這樣就不用每次計算了。JIT 分析後會對這段代碼進行優化,進行表達式提高的操做:

public void hoisting(int x) {

	int y = 654;

	int result = x * y;



	for (int i = 0; i < 1000; i = i + 1) {	

		// ...... 基於這個 result 變量的各類操做

	}

}

這樣一來,result 不用每次計算了,並且也徹底不影響執行結果,大大提高了執行效率。

注意,編譯器更喜歡局部變量,而不是靜態變量或者成員變量;由於靜態變量是「逃逸在外的」,多個線程均可以訪問到,而局部變量是線程私有的,不會被其餘線程訪問和修改。 ?

編譯器在處理靜態變量/成員變量時,會比較保守,不會輕易優化。 ?

像你問題裏的這個例子中,stopRequested就是個靜態變量,編譯器本不該該對其進行優化處理;

static boolean stopRequested = false;// 靜態變量



public static void main(String[] args) throws InterruptedException {



    Thread backgroundThread = new Thread(() -> {

        int i = 0;

        while (!stopRequested) {

			// leaf method

            i++;

        }

    }) ;

    backgroundThread.start();

    TimeUnit.MICROSECONDS.sleep(10);

    stopRequested = true ;

}

但因爲你這個循環是個 leaf method,即沒有調用任何方法,因此在循環之中不會有其餘線程會觀測到stopRequested值的變化。那麼編譯器就冒進的進行了表達式提高的操做,將stopRequested提高到表達式以外,做爲循環不變量(loop invariant)處理:

int i = 0;



boolean hoistedStopRequested = stopRequested;// 將stopRequested 提高爲局部變量

while (!hoistedStopRequested) {    

	i++;

}

這樣一來,最後將 stopRequested賦值爲 true 的操做,影響不了提高的hoistedStopRequested的值,天然就沒法影響循環的執行了,最終致使沒法退出。 ?

至於你增長了 println 以後,循環就能夠退出的問題。是由於你這行 println 代碼影響了編譯器的優化。println 方法因爲最終會調用 FileOutputStream.writeBytes 這個 native 方法,因此沒法被內聯優化(inling)。而未被內斂的方法調用從編譯器的角度看是一個「full memory kill」,也就是說 反作用不明 、必須對內存的讀寫操做作保守處理。 ?

在這個例子裏,下一輪循環的 stopRequested 讀取操做按順序要發生在上一輪循環的 println 以後。這裏「保守處理」爲:就算上一輪我已經讀取了 stopRequested 的值,因爲通過了一個反作用不明的地方,再到下一次訪問就必須從新讀取了。 ?

因此在你增長了 prinltln 以後,JIT 因爲要保守處理,從新讀取,天然就不能作上面的表達式提高優化了。 ?

以上對表達式提高的解釋,總結摘抄自 R大的知乎回答。 ?

我:「這下明白了吧,這都是 JIT 乾的好事,你要是禁用 JIT 就沒這問題了」

小夥伴:「臥槽????,一個簡單的 for 循環也太多機制了,沒想到 JIT 這麼智能,也沒想到 R 大這麼????」

小夥伴:「那 JIT 必定不少優化機制吧,除了這個表達式提高還有啥?」

我:我也不是搞編譯器的……哪瞭解這麼多,就知道一些經常使用的,簡單給你說說吧

表達式下沉(expression sinking)

和表達式提高相似的,還有個表達式下沉的優化,好比下面這段代碼:

public void sinking(int i) {

	int result = 543 * i;



	if (i % 2 == 0) {

		// 使用 result 值的一些邏輯代碼

	} else {

		// 一些不使用 result 的值的邏輯代碼

	}

}

因爲在 else 分支裏,並無使用 result 的值,可每次無論什麼分支都會先計算 result,這就不必了。JIT 會把 result 的計算表達式移動到 if 分支裏,這樣就避免了每次對 result 的計算,這個操做就叫表達式下沉:

public void sinking(int i) {

	if (i % 2 == 0) {

		int result = 543 * i;

		// 使用 result 值的一些邏輯代碼

	} else {

		// 一些不使用 result 的值的邏輯代碼

	}

}

JIT 還有那些常見優化?

技術學習總結

學習技術必定要制定一個明確的學習路線,這樣才能高效的學習,沒必要要作無效功,既浪費時間又得不到什麼效率,你們不妨按照我這份路線來學習。

最後面試分享

你們不妨直接在牛客和力扣上多刷題,同時,我也拿了一些面試題跟你們分享,也是從一些大佬那裏得到的,你們不妨多刷刷題,爲金九銀十衝一波!

最後,若須要完整pdf版,能夠點贊本文後點擊這裏免費領取

相關文章
相關標籤/搜索