自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

一個(gè) println 竟然比 volatile 還好使?

開(kāi)發(fā) 前端
在日常編碼過(guò)程中,不用刻意的猜測(cè) JIT 的優(yōu)化機(jī)制,JVM 也不會(huì)完整的告訴你所有的優(yōu)化。而且這種東西不同版本效果不一樣,就算搞明白了一個(gè)機(jī)制,可能到下個(gè)版本就會(huì)完全不一樣。

前兩天一個(gè)小伙伴突然找我求助,說(shuō)準(zhǔn)備換個(gè)坑,最近在系統(tǒng)復(fù)習(xí)多線程知識(shí),但遇到了一個(gè)刷新認(rèn)知的問(wèn)題……

小伙伴:Effective JAVA 里的并發(fā)章節(jié)里,有一段關(guān)于可見(jiàn)性的描述。下面這段代碼會(huì)出現(xiàn)死循環(huán),這個(gè)我能理解,JMM 內(nèi)存模型嘛,JMM 不保證 stopRequested 的修改能被及時(shí)的觀測(cè)到。

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 ;
}

但奇怪的是在我加了一行打印之后,就不會(huì)出現(xiàn)死循環(huán)了!難道我一行 println 能比 volatile 還好使啊?這倆也沒(méi)關(guān)系啊

static boolean stopRequested = false;

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

    Thread backgroundThread = new Thread(() -> {
        int i = 0;
        while (!stopRequested) {
            
            // 加上一行打印,循環(huán)就能退出了!
        	System.out.println(i++);
        }
    }) ;
    backgroundThread.start();
    TimeUnit.MICROSECONDS.sleep(10);
    stopRequested = true ;
}

我:小伙子八股文背的挺熟啊,JMM 張口就來(lái)。

我:這個(gè)……其實(shí)是 JIT 干的好事,導(dǎo)致你的循環(huán)無(wú)法退出。JMM 只是一個(gè)邏輯上的內(nèi)存模型規(guī)范,JIT可以根據(jù)JMM的規(guī)范來(lái)進(jìn)行優(yōu)化。

比如你第一個(gè)例子里,你用-Xint禁用 JIT,就可以退出死循環(huán)了,不信你試試?

小伙伴:WK,真的可以,加上 -Xint 循環(huán)就退出了,好神奇!JIT 是個(gè)啥啊?還能有這種功效?

JIT(Just-in-Time) 的優(yōu)化

眾所周知,JAVA 為了實(shí)現(xiàn)跨平臺(tái),增加了一層 JVM,不同平臺(tái)的 JVM 負(fù)責(zé)解釋執(zhí)行字節(jié)碼文件。雖然有一層解釋會(huì)影響效率,但好處是跨平臺(tái),字節(jié)碼文件是平臺(tái)無(wú)關(guān)的。

在 JAVA 1.2 之后,增加了 即時(shí)編譯(Just-in-Time Compilation,簡(jiǎn)稱 JIT) 的機(jī)制,在運(yùn)行時(shí)可以將執(zhí)行次數(shù)較多的熱點(diǎn)代碼編譯為機(jī)器碼,這樣就不需要 JVM 再解釋一遍了,可以直接執(zhí)行,增加運(yùn)行效率。

但 JIT 編譯器在編譯字節(jié)碼時(shí),可不僅僅是簡(jiǎn)單的直接將字節(jié)碼翻譯成機(jī)器碼,它在編譯的同時(shí)還會(huì)做很多優(yōu)化,比如循環(huán)展開(kāi)、方法內(nèi)聯(lián)等等……

這個(gè)問(wèn)題出現(xiàn)的原因,就是因?yàn)?JIT 編譯器的優(yōu)化技術(shù)之一 - 表達(dá)式提升(expression hoisting) 導(dǎo)致的。

表達(dá)式提升(expression hoisting)

先來(lái)看個(gè)例子,在這個(gè) hoisting 方法中,for 循環(huán)里每次都會(huì)定義一個(gè)變量 y,然后通過(guò)將 x*y 的結(jié)果存儲(chǔ)在一個(gè) result 變量中,然后使用這個(gè)變量進(jìn)行各種操作

public void hoisting(int x) {
	for (int i = 0; i < 1000; i = i + 1) {
		// 循環(huán)不變的計(jì)算 
		int y = 654;
		int result = x * y;
		
		// ...... 基于這個(gè) result 變量的各種操作
	}
}

但是這個(gè)例子里,result 的結(jié)果是固定的,并不會(huì)跟著循環(huán)而更新。所以完全可以將 result 的計(jì)算提取到循環(huán)之外,這樣就不用每次計(jì)算了。JIT 分析后會(huì)對(duì)這段代碼進(jìn)行優(yōu)化,進(jìn)行表達(dá)式提升的操作:

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

這樣一來(lái),result 不用每次計(jì)算了,而且也完全不影響執(zhí)行結(jié)果,大大提升了執(zhí)行效率。

注意,編譯器更喜歡局部變量,而不是靜態(tài)變量或者成員變量;因?yàn)殪o態(tài)變量是“逃逸在外的”,多個(gè)線程都可以訪問(wèn)到,而局部變量是線程私有的,不會(huì)被其他線程訪問(wèn)和修改。

編譯器在處理靜態(tài)變量/成員變量時(shí),會(huì)比較保守,不會(huì)輕易優(yōu)化。

像你問(wèn)題里的這個(gè)例子中,stopRequested就是個(gè)靜態(tài)變量,編譯器本不應(yīng)該對(duì)其進(jìn)行優(yōu)化處理;

static boolean stopRequested = false;// 靜態(tài)變量

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 ;
}

但由于你這個(gè)循環(huán)是個(gè) leaf method,即沒(méi)有調(diào)用任何方法,所以在循環(huán)之中不會(huì)有其他線程會(huì)觀測(cè)到stopRequested值的變化。那么編譯器就冒進(jìn)的進(jìn)行了表達(dá)式提升的操作,將stopRequested提升到表達(dá)式之外,作為循環(huán)不變量(loop invariant)處理:

int i = 0;

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

這樣一來(lái),最后將 stopRequested賦值為 true 的操作,影響不了提升的hoistedStopRequested的值,自然就無(wú)法影響循環(huán)的執(zhí)行了,最終導(dǎo)致無(wú)法退出。

至于你增加了 println 之后,循環(huán)就可以退出的問(wèn)題。是因?yàn)槟氵@行 println 代碼影響了編譯器的優(yōu)化。println 方法由于最終會(huì)調(diào)用
FileOutputStream.writeBytes 這個(gè) native 方法,所以無(wú)法被內(nèi)聯(lián)優(yōu)化(inling)。而未被內(nèi)斂的方法調(diào)用從編譯器的角度看是一個(gè)“full memory kill”,也就是說(shuō) 副作用不明 、必須對(duì)內(nèi)存的讀寫操作做保守處理。

在這個(gè)例子里,下一輪循環(huán)的 stopRequested 讀取操作按順序要發(fā)生在上一輪循環(huán)的 println 之后。這里“保守處理”為:就算上一輪我已經(jīng)讀取了 stopRequested 的值,由于經(jīng)過(guò)了一個(gè)副作用不明的地方,再到下一次訪問(wèn)就必須重新讀取了。

所以在你增加了 prinltln 之后,JIT 由于要保守處理,重新讀取,自然就不能做上面的表達(dá)式提升優(yōu)化了。

以上對(duì)表達(dá)式提升的解釋,總結(jié)摘抄自 R大的知乎回答。R大,行走的 JVM Wiki!

我:“這下明白了吧,這都是 JIT 干的好事,你要是禁用 JIT 就沒(méi)這問(wèn)題了”

小伙伴:“WK,一個(gè)簡(jiǎn)單的 for 循環(huán)也太多機(jī)制了,沒(méi)想到 JIT 這么智能,也沒(méi)想到 R 大這么”

小伙伴:“那 JIT 一定很多優(yōu)化機(jī)制吧,除了這個(gè)表達(dá)式提升還有啥?”

我:我也不是搞編譯器的……哪了解這么多,就知道一些常用的,簡(jiǎn)單給你說(shuō)說(shuō)吧

表達(dá)式下沉(expression sinking)

和表達(dá)式提升類似的,還有個(gè)表達(dá)式下沉的優(yōu)化,比如下面這段代碼:

public void sinking(int i) {
	int result = 543 * i;

	if (i % 2 == 0) {
		// 使用 result 值的一些邏輯代碼
	} else {
		// 一些不使用 result 的值的邏輯代碼
	}
}

由于在 else 分支里,并沒(méi)有使用 result 的值,可每次不管什么分支都會(huì)先計(jì)算 result,這就沒(méi)必要了。JIT 會(huì)把 result 的計(jì)算表達(dá)式移動(dòng)到 if 分支里,這樣就避免了每次對(duì) result 的計(jì)算,這個(gè)操作就叫表達(dá)式下沉:

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

JIT 還有那些常見(jiàn)優(yōu)化?

除了上面介紹的表達(dá)式提升/表達(dá)式下沉以外,還有一些常見(jiàn)的編譯器優(yōu)化機(jī)制。

循環(huán)展開(kāi)(Loop unwinding/loop unrolling)

下面這個(gè) for 循環(huán),一共要循環(huán) 10w 次,每次都需要檢查條件。

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

在編譯器的優(yōu)化后,會(huì)刪除一定的循環(huán)次數(shù),從而降低索引遞增和條件檢查操作而引起的開(kāi)銷:

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

除了循環(huán)展開(kāi),循環(huán)還有一些優(yōu)化機(jī)制,比如循環(huán)剝離、循環(huán)交換、循環(huán)分裂、循環(huán)合并……

內(nèi)聯(lián)優(yōu)化(Inling)

JVM 的方法調(diào)用是個(gè)棧的模型,每次方法調(diào)用都需要一個(gè)壓棧(push)和出棧(pop)的操作,編譯器也會(huì)對(duì)調(diào)用模型進(jìn)行優(yōu)化,將一些方法的調(diào)用進(jìn)行內(nèi)聯(lián)。

內(nèi)聯(lián)就是抽取要調(diào)用的方法體代碼,到當(dāng)前方法中直接執(zhí)行,這樣就可以避免一次壓棧出棧的操作,提升執(zhí)行效率。比如下面這個(gè)方法:

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;
}

在編譯器內(nèi)聯(lián)優(yōu)化后,會(huì)將 calculate 的方法體抽取到 inline 方法中,直接執(zhí)行,而不用進(jìn)行方法調(diào)用:

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

不過(guò)這個(gè)內(nèi)聯(lián)優(yōu)化是有一些限制的,比如 native 的方法就不能內(nèi)聯(lián)優(yōu)化

提前置空

來(lái)先看一個(gè)例子,在這個(gè)例子中 was finalized! 會(huì)在 done.之前輸出,這個(gè)也是因?yàn)?JIT 的優(yōu)化導(dǎo)致的。

class A {
    // 對(duì)象被回收前,會(huì)觸發(fā) 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.");
    }
}

//打印結(jié)果
Created A@1be6f5c3
A@1be6f5c3 was finalized!//finalize方法輸出
done.

從例子中可以看到,如果 a 在循環(huán)完成后已經(jīng)不再使用了,則會(huì)出現(xiàn)先執(zhí)行finalize的情況;雖然從對(duì)象作用域來(lái)說(shuō),方法沒(méi)有執(zhí)行完,棧幀并沒(méi)有出棧,但是還是會(huì)被提前執(zhí)行。

這就是因?yàn)?JIT 認(rèn)為 a 對(duì)象在循環(huán)內(nèi)和循環(huán)后都不會(huì)在使用,所以提前給它置空了,幫助 GC 回收;如果禁用 JIT,那就不會(huì)出現(xiàn)這個(gè)問(wèn)題。

這個(gè)提前回收的機(jī)制,還是有點(diǎn)風(fēng)險(xiǎn)的,在某些場(chǎng)景下可能會(huì)引起 BUG……

HotSpot VM JIT 的各種優(yōu)化項(xiàng)

上面只是介紹了幾個(gè)簡(jiǎn)單常用的編譯優(yōu)化機(jī)制,JVM JIT 更多的優(yōu)化機(jī)制可以參考下面這個(gè)圖。這是 OpenJDK 文檔中提供的一個(gè) pdf 材料,里面列出了 HotSpot JVM 的各種優(yōu)化機(jī)制,相當(dāng)多……

如何避免因 JIT 導(dǎo)致的問(wèn)題?

小伙伴:“JIT 這么多優(yōu)化機(jī)制,很容易出問(wèn)題啊,我平時(shí)寫代碼要怎么避開(kāi)這些呢”

平時(shí)在編碼的時(shí)候,不用刻意的去關(guān)心 JIT 的優(yōu)化,就比如上面那個(gè) println 問(wèn)題,JMM 本來(lái)就不保證修改對(duì)其他線程可見(jiàn),如果按照規(guī)范去加鎖或者用 volatile 修飾,根本就不會(huì)有這種問(wèn)題。

而那個(gè)提前置空導(dǎo)致的問(wèn)題,出現(xiàn)的幾率也很低,只要你規(guī)范寫代碼基本不會(huì)遇到的。

我:所以,這不是 JIT 的鍋,是你的……

小伙伴:“懂了,你這是說(shuō)我菜,說(shuō)我代碼寫的屎啊……”

總結(jié)

在日常編碼過(guò)程中,不用刻意的猜測(cè) JIT 的優(yōu)化機(jī)制,JVM 也不會(huì)完整的告訴你所有的優(yōu)化。而且這種東西不同版本效果不一樣,就算搞明白了一個(gè)機(jī)制,可能到下個(gè)版本就會(huì)完全不一樣。

所以,如果不是搞編譯器開(kāi)發(fā)的話,JIT 相關(guān)的編譯知識(shí),作為一個(gè)知識(shí)儲(chǔ)備就好。

也不用去猜測(cè) JIT 到底會(huì)怎么優(yōu)化你的代碼,你(可能)猜不準(zhǔn)……

本故事純屬瞎編,請(qǐng)勿隨意對(duì)號(hào)入座

參考

  • 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 大的知乎百科

一點(diǎn)補(bǔ)充

可能部分讀者大佬們會(huì)認(rèn)為是 sync 導(dǎo)致的問(wèn)題,下面是稍加改造后的 sync 例子,結(jié)果是仍然無(wú)法退出死循環(huán)……

public class HoistingTest {
	static boolean stopRequested = false;

	public static void main(String[] args) throws InterruptedException {
		Thread backgroundThread = new Thread(() -> {
			int i = 0;
			while (!stopRequested) {

				// 加上一行打印,循環(huán)就能退出了!
//				System.out.println(i++);
				new HoistingTest().test();
			}
		}) ;
		backgroundThread.start();
		TimeUnit.SECONDS.sleep(5);
		stopRequested = true ;
	}

	Object lock = new Object();

	private  void test(){

		synchronized (lock){}
	}
}

再升級(jí)下,把 test 方法,也加上 sync,結(jié)果還是無(wú)法退出死循環(huán)……

Object lock = new Object();

private synchronized void test(){

        synchronized (lock){}
}

但我只是想說(shuō),這個(gè)問(wèn)題的關(guān)鍵是 jit 的優(yōu)化導(dǎo)致的問(wèn)題。jmm 只是規(guī)范,而 jit 的優(yōu)化機(jī)制,也會(huì)遵循 jmm 的規(guī)范。

不過(guò) jmm 并沒(méi)有說(shuō) sync 會(huì)影響 jit 之類的,可就算 sync 會(huì)影響那又怎么樣呢……并不是關(guān)鍵點(diǎn)

結(jié)合 R大 的解釋,編譯器對(duì)靜態(tài)變量更敏感,如果把上面的 lock 對(duì)象修改成 static 的,循環(huán)又可以退出了……

那如果不加 static ,把 sync 換成 unsafe.pageSize()呢?結(jié)果是循環(huán)還是可以退出……

所以,本文的重點(diǎn)是描述 jit 的影響,而不是各種會(huì)影響 jit 的動(dòng)作。影響 jit 的可能性會(huì)非常多,而且不同的vm甚至不同的版本表現(xiàn)都會(huì)有所不同,我們并不需要去摸清這個(gè)機(jī)制,也沒(méi)法摸清(畢竟不是做編譯器的,就是是做編譯器,也不一定是 HotSpot……)

作者:京東保險(xiǎn) 蔣信

來(lái)源:京東云開(kāi)發(fā)者社區(qū) 

責(zé)任編輯:武曉燕 來(lái)源: 今日頭條
相關(guān)推薦

2021-04-11 11:23:00

Windows 10Windows微軟

2023-03-13 08:09:03

Protobuffeature分割

2024-03-15 08:18:25

volatileAtomic關(guān)鍵字

2023-01-12 08:47:26

二項(xiàng)式楊輝斐波那契

2015-11-12 13:47:53

Firefox OSAPPFirefox

2022-10-26 10:15:53

GoFramePHP數(shù)組

2020-10-07 22:26:02

微信工具電腦

2021-03-01 14:44:17

AI 數(shù)據(jù)人工智能

2023-09-14 08:46:50

ReactVue

2020-11-20 10:50:01

Docker容器

2010-11-11 09:13:58

超高密度服務(wù)器HP戴爾

2021-07-31 07:11:01

WPSExcel軟件

2023-03-09 08:13:34

2021-01-07 05:12:27

Persepolis應(yīng)用下載神器

2021-06-15 05:15:30

DeepL翻譯神器應(yīng)用

2022-07-07 08:59:37

requestsPython爬蟲(chóng)框架

2020-06-18 15:53:06

Python代碼摳圖

2021-05-27 07:54:21

JavaStateAQS

2021-05-20 10:42:58

Windows 10Windows微軟

2021-05-06 10:52:09

Java Spring Bo框架
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)