AVt天堂网 手机版,亚洲va久久久噜噜噜久久4399,天天综合亚洲色在线精品,亚洲一级Av无码毛片久久精品

當前位置:首頁 > 科技  > 軟件

一個 println 竟然比 volatile 還好使?

來源: 責編: 時間:2023-10-08 09:59:58 274觀看
導讀前兩天一個小伙伴突然找我求助,說準備換個坑,最近在系統復習多線程知識,但遇到了一個刷新認知的問題……小伙伴:Effective JAVA 里的并發章節里,有一段關于可見性的描述。下面這段代碼會出現死循環,這個我能理解,JMM 內存模

前兩天一個小伙伴突然找我求助,說準備換個坑,最近在系統復習多線程知識,但遇到了一個刷新認知的問題……wjl28資訊網——每日最新資訊28at.com

小伙伴:Effective JAVA 里的并發章節里,有一段關于可見性的描述。下面這段代碼會出現死循環,這個我能理解,JMM 內存模型嘛,JMM 不保證 stopRequested 的修改能被及時的觀測到。wjl28資訊網——每日最新資訊28at.com

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 還好使啊?這倆也沒關系啊wjl28資訊網——每日最新資訊28at.com

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 張口就來。wjl28資訊網——每日最新資訊28at.com

我:這個……其實是 JIT 干的好事,導致你的循環無法退出。JMM 只是一個邏輯上的內存模型規范,JIT可以根據JMM的規范來進行優化。wjl28資訊網——每日最新資訊28at.com

比如你第一個例子里,你用-Xint禁用 JIT,就可以退出死循環了,不信你試試?wjl28資訊網——每日最新資訊28at.com

小伙伴:WK,真的可以,加上 -Xint 循環就退出了,好神奇!JIT 是個啥啊?還能有這種功效?wjl28資訊網——每日最新資訊28at.com

JIT(Just-in-Time) 的優化

眾所周知,JAVA 為了實現跨平臺,增加了一層 JVM,不同平臺的 JVM 負責解釋執行字節碼文件。雖然有一層解釋會影響效率,但好處是跨平臺,字節碼文件是平臺無關的。wjl28資訊網——每日最新資訊28at.com

wjl28資訊網——每日最新資訊28at.com

在 JAVA 1.2 之后,增加了 即時編譯(Just-in-Time Compilation,簡稱 JIT) 的機制,在運行時可以將執行次數較多的熱點代碼編譯為機器碼,這樣就不需要 JVM 再解釋一遍了,可以直接執行,增加運行效率。wjl28資訊網——每日最新資訊28at.com

wjl28資訊網——每日最新資訊28at.com

但 JIT 編譯器在編譯字節碼時,可不僅僅是簡單的直接將字節碼翻譯成機器碼,它在編譯的同時還會做很多優化,比如循環展開、方法內聯等等……
wjl28資訊網——每日最新資訊28at.com

這個問題出現的原因,就是因為 JIT 編譯器的優化技術之一 - 表達式提升(expression hoisting) 導致的。wjl28資訊網——每日最新資訊28at.com

表達式提升(expression hoisting)

先來看個例子,在這個 hoisting 方法中,for 循環里每次都會定義一個變量 y,然后通過將 x*y 的結果存儲在一個 result 變量中,然后使用這個變量進行各種操作wjl28資訊網——每日最新資訊28at.com

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 分析后會對這段代碼進行優化,進行表達式提升的操作:wjl28資訊網——每日最新資訊28at.com

public void hoisting(int x) {	int y = 654;	int result = x * y;    	for (int i = 0; i < 1000; i = i + 1) {			// ...... 基于這個 result 變量的各種操作	}}

這樣一來,result 不用每次計算了,而且也完全不影響執行結果,大大提升了執行效率。wjl28資訊網——每日最新資訊28at.com

注意,編譯器更喜歡局部變量,而不是靜態變量或者成員變量;因為靜態變量是“逃逸在外的”,多個線程都可以訪問到,而局部變量是線程私有的,不會被其他線程訪問和修改。wjl28資訊網——每日最新資訊28at.com

編譯器在處理靜態變量/成員變量時,會比較保守,不會輕易優化。wjl28資訊網——每日最新資訊28at.com

像你問題里的這個例子中,stopRequested就是個靜態變量,編譯器本不應該對其進行優化處理;wjl28資訊網——每日最新資訊28at.com

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)處理:wjl28資訊網——每日最新資訊28at.com

int i = 0;boolean hoistedStopRequested = stopRequested;// 將stopRequested 提升為局部變量while (!hoistedStopRequested) {    	i++;}

這樣一來,最后將 stopRequested賦值為 true 的操作,影響不了提升的hoistedStopRequested的值,自然就無法影響循環的執行了,最終導致無法退出。wjl28資訊網——每日最新資訊28at.com

至于你增加了 println 之后,循環就可以退出的問題。是因為你這行 println 代碼影響了編譯器的優化。println 方法由于最終會調用
FileOutputStream.writeBytes 這個 native 方法,所以無法被內聯優化(inling)。而未被內斂的方法調用從編譯器的角度看是一個“full memory kill”,也就是說 副作用不明 、必須對內存的讀寫操作做保守處理。wjl28資訊網——每日最新資訊28at.com

在這個例子里,下一輪循環的 stopRequested 讀取操作按順序要發生在上一輪循環的 println 之后。這里“保守處理”為:就算上一輪我已經讀取了 stopRequested 的值,由于經過了一個副作用不明的地方,再到下一次訪問就必須重新讀取了。wjl28資訊網——每日最新資訊28at.com

所以在你增加了 prinltln 之后,JIT 由于要保守處理,重新讀取,自然就不能做上面的表達式提升優化了。wjl28資訊網——每日最新資訊28at.com

以上對表達式提升的解釋,總結摘抄自 R大的知乎回答。R大,行走的 JVM Wiki!
wjl28資訊網——每日最新資訊28at.com

我:“這下明白了吧,這都是 JIT 干的好事,你要是禁用 JIT 就沒這問題了”wjl28資訊網——每日最新資訊28at.com

小伙伴:“WK,一個簡單的 for 循環也太多機制了,沒想到 JIT 這么智能,也沒想到 R 大這么”wjl28資訊網——每日最新資訊28at.com

小伙伴:“那 JIT 一定很多優化機制吧,除了這個表達式提升還有啥?”wjl28資訊網——每日最新資訊28at.com

我:我也不是搞編譯器的……哪了解這么多,就知道一些常用的,簡單給你說說吧wjl28資訊網——每日最新資訊28at.com

表達式下沉(expression sinking)

和表達式提升類似的,還有個表達式下沉的優化,比如下面這段代碼:wjl28資訊網——每日最新資訊28at.com

public void sinking(int i) {	int result = 543 * i;	if (i % 2 == 0) {		// 使用 result 值的一些邏輯代碼	} else {		// 一些不使用 result 的值的邏輯代碼	}}

由于在 else 分支里,并沒有使用 result 的值,可每次不管什么分支都會先計算 result,這就沒必要了。JIT 會把 result 的計算表達式移動到 if 分支里,這樣就避免了每次對 result 的計算,這個操作就叫表達式下沉:wjl28資訊網——每日最新資訊28at.com

public void sinking(int i) {	if (i % 2 == 0) {		int result = 543 * i;		// 使用 result 值的一些邏輯代碼	} else {		// 一些不使用 result 的值的邏輯代碼	}}

JIT 還有那些常見優化?

除了上面介紹的表達式提升/表達式下沉以外,還有一些常見的編譯器優化機制。wjl28資訊網——每日最新資訊28at.com

循環展開(Loop unwinding/loop unrolling)

下面這個 for 循環,一共要循環 10w 次,每次都需要檢查條件。wjl28資訊網——每日最新資訊28at.com

for (int i = 0; i < 100000; i++) {    delete(i);}

在編譯器的優化后,會刪除一定的循環次數,從而降低索引遞增和條件檢查操作而引起的開銷:wjl28資訊網——每日最新資訊28at.com

for (int i = 0; i < 20000; i+=5) {    delete(i);    delete(i + 1);    delete(i + 2);    delete(i + 3);    delete(i + 4);}

除了循環展開,循環還有一些優化機制,比如循環剝離、循環交換、循環分裂、循環合并……wjl28資訊網——每日最新資訊28at.com

內聯優化(Inling)

JVM 的方法調用是個棧的模型,每次方法調用都需要一個壓棧(push)和出棧(pop)的操作,編譯器也會對調用模型進行優化,將一些方法的調用進行內聯。
wjl28資訊網——每日最新資訊28at.com

內聯就是抽取要調用的方法體代碼,到當前方法中直接執行,這樣就可以避免一次壓棧出棧的操作,提升執行效率。比如下面這個方法:wjl28資訊網——每日最新資訊28at.com

public  void inline(){	int a = 5;    int b = 10;    int c = calculate(a, b);        // 使用 c 處理……}public int calculate(int a, int b){	return a + b;}

在編譯器內聯優化后,會將 calculate 的方法體抽取到 inline 方法中,直接執行,而不用進行方法調用:wjl28資訊網——每日最新資訊28at.com

public  void inline(){	int a = 5;    int b = 10;    int c = a + b;        // 使用 c 處理……}

不過這個內聯優化是有一些限制的,比如 native 的方法就不能內聯優化wjl28資訊網——每日最新資訊28at.com

提前置空

來先看一個例子,在這個例子中 was finalized! 會在 done.之前輸出,這個也是因為 JIT 的優化導致的。wjl28資訊網——每日最新資訊28at.com

class A {    // 對象被回收前,會觸發 finalize    @Override protected void finalize() {        System.out.println(this + " was finalized!");    }    public static void main(String[] args) throws InterruptedException {        A a = new A();        System.out.println("Created " + a);        for (int i = 0; i < 1_000_000_000; i++) {            if (i % 1_000_00 == 0)                System.gc();        }        System.out.println("done.");    }}//打印結果Created A@1be6f5c3A@1be6f5c3 was finalized!//finalize方法輸出done.

從例子中可以看到,如果 a 在循環完成后已經不再使用了,則會出現先執行finalize的情況;雖然從對象作用域來說,方法沒有執行完,棧幀并沒有出棧,但是還是會被提前執行。wjl28資訊網——每日最新資訊28at.com

這就是因為 JIT 認為 a 對象在循環內和循環后都不會在使用,所以提前給它置空了,幫助 GC 回收;如果禁用 JIT,那就不會出現這個問題。wjl28資訊網——每日最新資訊28at.com

這個提前回收的機制,還是有點風險的,在某些場景下可能會引起 BUG……wjl28資訊網——每日最新資訊28at.com

HotSpot VM JIT 的各種優化項

上面只是介紹了幾個簡單常用的編譯優化機制,JVM JIT 更多的優化機制可以參考下面這個圖。這是 OpenJDK 文檔中提供的一個 pdf 材料,里面列出了 HotSpot JVM 的各種優化機制,相當多……wjl28資訊網——每日最新資訊28at.com

wjl28資訊網——每日最新資訊28at.com

如何避免因 JIT 導致的問題?

小伙伴:“JIT 這么多優化機制,很容易出問題啊,我平時寫代碼要怎么避開這些呢”wjl28資訊網——每日最新資訊28at.com

平時在編碼的時候,不用刻意的去關心 JIT 的優化,就比如上面那個 println 問題,JMM 本來就不保證修改對其他線程可見,如果按照規范去加鎖或者用 volatile 修飾,根本就不會有這種問題。wjl28資訊網——每日最新資訊28at.com

而那個提前置空導致的問題,出現的幾率也很低,只要你規范寫代碼基本不會遇到的。wjl28資訊網——每日最新資訊28at.com

我:所以,這不是 JIT 的鍋,是你的……wjl28資訊網——每日最新資訊28at.com

小伙伴:“懂了,你這是說我菜,說我代碼寫的屎啊……”wjl28資訊網——每日最新資訊28at.com

總結

在日常編碼過程中,不用刻意的猜測 JIT 的優化機制,JVM 也不會完整的告訴你所有的優化。而且這種東西不同版本效果不一樣,就算搞明白了一個機制,可能到下個版本就會完全不一樣。wjl28資訊網——每日最新資訊28at.com

所以,如果不是搞編譯器開發的話,JIT 相關的編譯知識,作為一個知識儲備就好。wjl28資訊網——每日最新資訊28at.com

也不用去猜測 JIT 到底會怎么優化你的代碼,你(可能)猜不準……wjl28資訊網——每日最新資訊28at.com

本故事純屬瞎編,請勿隨意對號入座wjl28資訊網——每日最新資訊28at.com

參考

  • JSR-133 Java Memory Model and Thread Specification 1.0 Proposed Final Draft
  • Oracle JVM Just-in-Time Compiler (JIT)
  • JVM JIT-compiler overview - Vladimir Ivanov HotSpot JVM Compiler Oracle Corp.
  • JVM JIT optimization techniques - part 2
  • The Java platform - WikiBook
  • R 大的知乎百科

一點補充

可能部分讀者大佬們會認為是 sync 導致的問題,下面是稍加改造后的 sync 例子,結果是仍然無法退出死循環……wjl28資訊網——每日最新資訊28at.com

public class HoistingTest {	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++);				new HoistingTest().test();			}		}) ;		backgroundThread.start();		TimeUnit.SECONDS.sleep(5);		stopRequested = true ;	}	Object lock = new Object();	private  void test(){		synchronized (lock){}	}}

再升級下,把 test 方法,也加上 sync,結果還是無法退出死循環……wjl28資訊網——每日最新資訊28at.com

Object lock = new Object();private synchronized void test(){        synchronized (lock){}}

但我只是想說,這個問題的關鍵是 jit 的優化導致的問題。jmm 只是規范,而 jit 的優化機制,也會遵循 jmm 的規范。wjl28資訊網——每日最新資訊28at.com

不過 jmm 并沒有說 sync 會影響 jit 之類的,可就算 sync 會影響那又怎么樣呢……并不是關鍵點wjl28資訊網——每日最新資訊28at.com

結合 R大 的解釋,編譯器對靜態變量更敏感,如果把上面的 lock 對象修改成 static 的,循環又可以退出了……wjl28資訊網——每日最新資訊28at.com

那如果不加 static ,把 sync 換成 unsafe.pageSize()呢?結果是循環還是可以退出……wjl28資訊網——每日最新資訊28at.com

所以,本文的重點是描述 jit 的影響,而不是各種會影響 jit 的動作。影響 jit 的可能性會非常多,而且不同的vm甚至不同的版本表現都會有所不同,我們并不需要去摸清這個機制,也沒法摸清(畢竟不是做編譯器的,就是是做編譯器,也不一定是 HotSpot……)wjl28資訊網——每日最新資訊28at.com

作者:京東保險 蔣信wjl28資訊網——每日最新資訊28at.com

來源:京東云開發者社區 wjl28資訊網——每日最新資訊28at.com

本文鏈接:http://www.tebozhan.com/showinfo-26-12432-0.html一個 println 竟然比 volatile 還好使?

聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com

上一篇: 圖形編輯器開發:快捷鍵的管理

下一篇: 如何實現并部署自己的Npm解析服務

標簽:
  • 熱門焦點
Top