前兩天一個小夥伴忽然找我求助,說準備換個坑,最近在系統學習多線程知識,但遇到了一個刷新認知的問題……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 是個啥啊?還能有這種功效?併發
衆所周知,JAVA 爲了實現跨平臺,增長了一層 JVM,不一樣平臺的 JVM 負責解釋執行字節碼文件。雖然有一層解釋會影響效率,但好處是跨平臺,字節碼文件是平臺無關的。
在 JAVA 1.2 以後,增長了 即時編譯(Just-in-Time Compilation,簡稱 JIT) 的機制,在運行時能夠將執行次數較多的熱點代碼編譯爲機器碼,這樣就不須要 JVM 再解釋一遍了,能夠直接執行,增長運行效率。
?
但 JIT 編譯器在編譯字節碼時,可不只僅是簡單的直接將字節碼翻譯成機器碼,它在編譯的同時還會作不少優化,好比循環展開、方法內聯等等…… ?
這個問題出現的緣由,就是由於 JIT 編譯器的優化技術之一 - 表達式提高(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 必定不少優化機制吧,除了這個表達式提高還有啥?」
我:我也不是搞編譯器的……哪瞭解這麼多,就知道一些經常使用的,簡單給你說說吧
和表達式提高相似的,還有個表達式下沉的優化,好比下面這段代碼:
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 的值的邏輯代碼 } }
學習技術必定要制定一個明確的學習路線,這樣才能高效的學習,沒必要要作無效功,既浪費時間又得不到什麼效率,你們不妨按照我這份路線來學習。
你們不妨直接在牛客和力扣上多刷題,同時,我也拿了一些面試題跟你們分享,也是從一些大佬那裏得到的,你們不妨多刷刷題,爲金九銀十衝一波!
最後,若須要完整pdf版,能夠點贊本文後點擊這裏免費領取