前兩天一個小伙伴突然找我求助,說準備換個坑,最近在系統複習多線程知識,但遇到了一個刷新認知的問題…… ...
前兩天一個小伙伴突然找我求助,說準備換個坑,最近在系統複習多線程知識,但遇到了一個刷新認知的問題……
小伙伴: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 還好使啊?這倆也沒關係啊
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可以根據JMM的規範來進行優化。
比如你第一個例子里,你用
-Xint
禁用 JIT,就可以退出死迴圈了,不信你試試?
小伙伴:WK,真的可以,加上 -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大的知乎回答。R大,行走的 JVM Wiki!
我:“這下明白了吧,這都是 JIT 乾的好事,你要是禁用 JIT 就沒這問題了”
小伙伴:“WK