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

當(dāng)前位置:首頁(yè) > 科技  > 軟件

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

來源: 責(zé)編: 時(shí)間:2023-10-08 09:59:58 306觀看
導(dǎo)讀前兩天一個(gè)小伙伴突然找我求助,說準(zhǔn)備換個(gè)坑,最近在系統(tǒng)復(fù)習(xí)多線程知識(shí),但遇到了一個(gè)刷新認(rèn)知的問題……小伙伴:Effective JAVA 里的并發(fā)章節(jié)里,有一段關(guān)于可見性的描述。下面這段代碼會(huì)出現(xiàn)死循環(huán),這個(gè)我能理解,JMM 內(nèi)存模

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

小伙伴:Effective JAVA 里的并發(fā)章節(jié)里,有一段關(guān)于可見性的描述。下面這段代碼會(huì)出現(xiàn)死循環(huán),這個(gè)我能理解,JMM 內(nèi)存模型嘛,JMM 不保證 stopRequested 的修改能被及時(shí)的觀測(cè)到。whr28資訊網(wǎng)——每日最新資訊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 ;}

但奇怪的是在我加了一行打印之后,就不會(huì)出現(xiàn)死循環(huán)了!難道我一行 println 能比 volatile 還好使啊?這倆也沒關(guān)系啊whr28資訊網(wǎng)——每日最新資訊28at.com

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 張口就來。whr28資訊網(wǎng)——每日最新資訊28at.com

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

比如你第一個(gè)例子里,你用-Xint禁用 JIT,就可以退出死循環(huán)了,不信你試試?whr28資訊網(wǎng)——每日最新資訊28at.com

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

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)無關(guān)的。whr28資訊網(wǎng)——每日最新資訊28at.com

whr28資訊網(wǎng)——每日最新資訊28at.com

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

whr28資訊網(wǎng)——每日最新資訊28at.com

但 JIT 編譯器在編譯字節(jié)碼時(shí),可不僅僅是簡(jiǎn)單的直接將字節(jié)碼翻譯成機(jī)器碼,它在編譯的同時(shí)還會(huì)做很多優(yōu)化,比如循環(huán)展開、方法內(nèi)聯(lián)等等……
whr28資訊網(wǎng)——每日最新資訊28at.com

這個(gè)問題出現(xiàn)的原因,就是因?yàn)?JIT 編譯器的優(yōu)化技術(shù)之一 - 表達(dá)式提升(expression hoisting) 導(dǎo)致的。whr28資訊網(wǎng)——每日最新資訊28at.com

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

先來看個(gè)例子,在這個(gè) hoisting 方法中,for 循環(huán)里每次都會(huì)定義一個(gè)變量 y,然后通過將 x*y 的結(jié)果存儲(chǔ)在一個(gè) result 變量中,然后使用這個(gè)變量進(jìn)行各種操作whr28資訊網(wǎng)——每日最新資訊28at.com

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á)式提升的操作:whr28資訊網(wǎng)——每日最新資訊28at.com

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

這樣一來,result 不用每次計(jì)算了,而且也完全不影響執(zhí)行結(jié)果,大大提升了執(zhí)行效率。whr28資訊網(wǎng)——每日最新資訊28at.com

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

編譯器在處理靜態(tài)變量/成員變量時(shí),會(huì)比較保守,不會(huì)輕易優(yōu)化。whr28資訊網(wǎng)——每日最新資訊28at.com

像你問題里的這個(gè)例子中,stopRequested就是個(gè)靜態(tài)變量,編譯器本不應(yīng)該對(duì)其進(jìn)行優(yōu)化處理;whr28資訊網(wǎng)——每日最新資訊28at.com

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,即沒有調(diào)用任何方法,所以在循環(huán)之中不會(huì)有其他線程會(huì)觀測(cè)到stopRequested值的變化。那么編譯器就冒進(jìn)的進(jìn)行了表達(dá)式提升的操作,將stopRequested提升到表達(dá)式之外,作為循環(huán)不變量(loop invariant)處理:whr28資訊網(wǎng)——每日最新資訊28at.com

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

這樣一來,最后將 stopRequested賦值為 true 的操作,影響不了提升的hoistedStopRequested的值,自然就無法影響循環(huán)的執(zhí)行了,最終導(dǎo)致無法退出。whr28資訊網(wǎng)——每日最新資訊28at.com

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

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

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

以上對(duì)表達(dá)式提升的解釋,總結(jié)摘抄自 R大的知乎回答。R大,行走的 JVM Wiki!
whr28資訊網(wǎng)——每日最新資訊28at.com

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

小伙伴:“WK,一個(gè)簡(jiǎn)單的 for 循環(huán)也太多機(jī)制了,沒想到 JIT 這么智能,也沒想到 R 大這么”whr28資訊網(wǎng)——每日最新資訊28at.com

小伙伴:“那 JIT 一定很多優(yōu)化機(jī)制吧,除了這個(gè)表達(dá)式提升還有啥?”whr28資訊網(wǎng)——每日最新資訊28at.com

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

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

和表達(dá)式提升類似的,還有個(gè)表達(dá)式下沉的優(yōu)化,比如下面這段代碼:whr28資訊網(wǎng)——每日最新資訊28at.com

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

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

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

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

除了上面介紹的表達(dá)式提升/表達(dá)式下沉以外,還有一些常見的編譯器優(yōu)化機(jī)制。whr28資訊網(wǎng)——每日最新資訊28at.com

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

下面這個(gè) for 循環(huán),一共要循環(huán) 10w 次,每次都需要檢查條件。whr28資訊網(wǎng)——每日最新資訊28at.com

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

在編譯器的優(yōu)化后,會(huì)刪除一定的循環(huán)次數(shù),從而降低索引遞增和條件檢查操作而引起的開銷:whr28資訊網(wǎng)——每日最新資訊28at.com

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

除了循環(huán)展開,循環(huán)還有一些優(yōu)化機(jī)制,比如循環(huán)剝離、循環(huán)交換、循環(huán)分裂、循環(huán)合并……whr28資訊網(wǎng)——每日最新資訊28at.com

內(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)。
whr28資訊網(wǎng)——每日最新資訊28at.com

內(nèi)聯(lián)就是抽取要調(diào)用的方法體代碼,到當(dāng)前方法中直接執(zhí)行,這樣就可以避免一次壓棧出棧的操作,提升執(zhí)行效率。比如下面這個(gè)方法:whr28資訊網(wǎng)——每日最新資訊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;}

在編譯器內(nèi)聯(lián)優(yōu)化后,會(huì)將 calculate 的方法體抽取到 inline 方法中,直接執(zhí)行,而不用進(jìn)行方法調(diào)用:whr28資訊網(wǎng)——每日最新資訊28at.com

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

不過這個(gè)內(nèi)聯(lián)優(yōu)化是有一些限制的,比如 native 的方法就不能內(nèi)聯(lián)優(yōu)化whr28資訊網(wǎng)——每日最新資訊28at.com

提前置空

來先看一個(gè)例子,在這個(gè)例子中 was finalized! 會(huì)在 done.之前輸出,這個(gè)也是因?yàn)?JIT 的優(yōu)化導(dǎo)致的。whr28資訊網(wǎng)——每日最新資訊28at.com

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@1be6f5c3A@1be6f5c3 was finalized!//finalize方法輸出done.

從例子中可以看到,如果 a 在循環(huán)完成后已經(jīng)不再使用了,則會(huì)出現(xiàn)先執(zhí)行finalize的情況;雖然從對(duì)象作用域來說,方法沒有執(zhí)行完,棧幀并沒有出棧,但是還是會(huì)被提前執(zhí)行。whr28資訊網(wǎng)——每日最新資訊28at.com

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

這個(gè)提前回收的機(jī)制,還是有點(diǎn)風(fēng)險(xiǎn)的,在某些場(chǎng)景下可能會(huì)引起 BUG……whr28資訊網(wǎng)——每日最新資訊28at.com

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)多……whr28資訊網(wǎng)——每日最新資訊28at.com

whr28資訊網(wǎng)——每日最新資訊28at.com

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

小伙伴:“JIT 這么多優(yōu)化機(jī)制,很容易出問題啊,我平時(shí)寫代碼要怎么避開這些呢”whr28資訊網(wǎng)——每日最新資訊28at.com

平時(shí)在編碼的時(shí)候,不用刻意的去關(guān)心 JIT 的優(yōu)化,就比如上面那個(gè) println 問題,JMM 本來就不保證修改對(duì)其他線程可見,如果按照規(guī)范去加鎖或者用 volatile 修飾,根本就不會(huì)有這種問題。whr28資訊網(wǎng)——每日最新資訊28at.com

而那個(gè)提前置空導(dǎo)致的問題,出現(xiàn)的幾率也很低,只要你規(guī)范寫代碼基本不會(huì)遇到的。whr28資訊網(wǎng)——每日最新資訊28at.com

我:所以,這不是 JIT 的鍋,是你的……whr28資訊網(wǎng)——每日最新資訊28at.com

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

總結(jié)

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

所以,如果不是搞編譯器開發(fā)的話,JIT 相關(guān)的編譯知識(shí),作為一個(gè)知識(shí)儲(chǔ)備就好。whr28資訊網(wǎng)——每日最新資訊28at.com

也不用去猜測(cè) JIT 到底會(huì)怎么優(yōu)化你的代碼,你(可能)猜不準(zhǔn)……whr28資訊網(wǎng)——每日最新資訊28at.com

本故事純屬瞎編,請(qǐng)勿隨意對(duì)號(hào)入座whr28資訊網(wǎng)——每日最新資訊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 大的知乎百科

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

可能部分讀者大佬們會(huì)認(rèn)為是 sync 導(dǎo)致的問題,下面是稍加改造后的 sync 例子,結(jié)果是仍然無法退出死循環(huán)……whr28資訊網(wǎng)——每日最新資訊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) {				// 加上一行打印,循環(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é)果還是無法退出死循環(huán)……whr28資訊網(wǎng)——每日最新資訊28at.com

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

但我只是想說,這個(gè)問題的關(guān)鍵是 jit 的優(yōu)化導(dǎo)致的問題。jmm 只是規(guī)范,而 jit 的優(yōu)化機(jī)制,也會(huì)遵循 jmm 的規(guī)范。whr28資訊網(wǎng)——每日最新資訊28at.com

不過 jmm 并沒有說 sync 會(huì)影響 jit 之類的,可就算 sync 會(huì)影響那又怎么樣呢……并不是關(guān)鍵點(diǎn)whr28資訊網(wǎng)——每日最新資訊28at.com

結(jié)合 R大 的解釋,編譯器對(duì)靜態(tài)變量更敏感,如果把上面的 lock 對(duì)象修改成 static 的,循環(huán)又可以退出了……whr28資訊網(wǎng)——每日最新資訊28at.com

那如果不加 static ,把 sync 換成 unsafe.pageSize()呢?結(jié)果是循環(huán)還是可以退出……whr28資訊網(wǎng)——每日最新資訊28at.com

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

作者:京東保險(xiǎn) 蔣信whr28資訊網(wǎng)——每日最新資訊28at.com

來源:京東云開發(fā)者社區(qū) whr28資訊網(wǎng)——每日最新資訊28at.com

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

聲明:本網(wǎng)頁(yè)內(nèi)容旨在傳播知識(shí),若有侵權(quán)等問題請(qǐng)及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。郵件:2376512515@qq.com

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

下一篇: 如何實(shí)現(xiàn)并部署自己的Npm解析服務(wù)

標(biāo)簽:
  • 熱門焦點(diǎn)
Top