Java内存模型与指令重排
點擊上方?好好學java?,選擇?星標?公眾號
轉自:大道方圓
鏈接:cnblogs.com/xdecode/p/8948277.html
本文暫不深入講解?JMM(Java 內存模型)中的主存、工作內存以及數據如何在其中流轉等。因為這些本身還牽扯到硬件內存架構,直接上手容易繞暈。先從以下幾個點探索JMM:
原子性;
有序性;
可見性;
指令重排:CPU 指令重排、編譯器優化重排;
Happen-Before 規則。
原子性
原子性是指一個操作是不可中斷的。即使多個線程一起執行,一個操作一旦開始,就不會被其它線程干擾。例如 CPU 中的一些指令屬于原子性的,又或者變量直接賦值操作 (i = 1) 也是原子性的。即使有多個線程對 i 賦值相互也不會干擾。
而 i++ 則不是原子性的, 因為實際上它等價于 i = i + 1。若有多個線程操作 i,結果將不可預期。
有序性
有序性是指,在單線程環境中程序是按序依次執行的。而多線程環境中, 程序的執行可能因為指令重排而出現亂序,下文會有詳細講述。
class OrderExample {int a = 0;boolean flag = false;public void writer() {// 以下兩句執行順序可能會在指令重排等場景下發生變化a = 1;flag = true;}public void reader() {if (flag) {int i = a + 1;……}}}可見性
可見性是指,當一個線程修改了某一個共享變量的值,其他線程是否能夠立即知道這個修改。有多個場景會影響到可見性:
CPU 指令重排
多條匯編指令執行時, 考慮性能因素會導致執行亂序。下文會有詳細講述。
硬件優化(如寫吸收、批操作)
CPU2 修改了變量 T,而 CPU1 卻從高速緩存 cache 中讀取了之前 T 的副本,導致數據不一致。
編譯器優化
主要是 Java 虛擬機層面的可見性,下文會有詳細講述。
指令重排
指令重排是指在程序執行過程中,為了性能考慮編譯器和 CPU 可能會對指令重新排序。
CPU指令重排
一條匯編指令的執行是可以分為很多步驟得,分為不同的硬件執行:
取指 IF;
譯碼和取寄存器操作數 ID;
執行或者有效地址計算 EX(ALU 邏輯計算單元);
存儲器訪問 MEM;
寫回 WB(寄存器)。
既然指令可以被分解為很多步驟,那么多條指令就不一定依次序執行。
因為每次只執行一條指令依次執行效率太低了。假設上述每一個步驟都要消耗一個時鐘周期,那么依次執行的話一條指令要5個時鐘周期,兩條指令要占用10個時鐘周期,三條指令消耗15個時鐘。
而如果硬件空閑即可執行下一步,類似于工廠中的流水線,一條指令要5個時鐘周期。兩條指令只需要6個時鐘周期。因為是錯位流水執行,三條指令消耗7個時鐘。
舉個例子 A = B + C 需要如下指令:
指令1 : 加載 B 到寄存器 R1中;
指令2 : 加載 C 到寄存器 R2 中;
指令3 : 將 R1 與 R2 相加,得到 R3;
指令4 : 將 R3 賦值給 A。
注意下圖紅色框選部分:指令1、2獨立執行,互不干擾。指令3依賴于指令1、指令2加載結果,因此紅色框選部分表示在等待指令1、指令2結束。待指令1、指令2都已經走完 MEM 部分。數據加載到內存后,指令3繼續執行計算 EX。同理,指令4需要等指令3計算完才可以拿到 R3,因此也需要錯位等待。
再來看一個復雜的例子:
a = b + c d = e - f具體指令執行步驟如下圖,不再贅述。與上圖類似,在執行過程中同樣會出現等待。
這邊框選的 X 統稱一個氣泡。有沒有什么方案可以削減這類氣泡呢?
答案自然是可以的。我們可以在出現氣泡之前執行其他不相干指令來減少氣泡。例如,可以將第五步的加載 e 到寄存器提前執行,消除第一個氣泡。同理,將第六步的加載 f 到寄存器提前執行,消除第二個氣泡。
經過指令重排后,整個流水線會更加順暢,無氣泡阻塞執行。
原先需要14個時鐘周期的指令,重排后只需要12個時鐘周期即可執行完畢。指令重排只可能發生在毫無關系的指令之間,如果指令之間存在依賴關系則不會重排。例如:指令1為?a = 1,指令2為 b = a - 1。則指令1、指令2 不會發生重排。
編譯器優化
主要指 JVM 層面,如下面代碼:在 JVM Client 模式很快就跳出了 while 循環;而在 Server 模式下運行,永遠不會停止。
/*** Created by Administrator on 2018/5/3/0003.*/ public class VisibilityTest extends Thread {private boolean stop;public void run() {int i = 0;while (!stop) {i++;}System.out.println("finish loop,i=" + i);}public void stopIt() {stop = true;}public boolean getStop() {return stop;}public static void main(String[] args) throws Exception {VisibilityTest v = new VisibilityTest();v.start();Thread.sleep(1000);v.stopIt();Thread.sleep(2000);System.out.println("finish main");System.out.println(v.getStop());}}以32位 JDK 1.7.0_55為例,可以通過修改 JAVA_HOME/jre/lib/i386/jvm.cfg 將 JVM 調整為 Server 模式驗證。修改內容如下圖所示,將 -server 調整到 -client 的上面。
-server KNOWN -client KNOWN -hotspot ALIASED_TO -client -classic WARN -native ERROR -green ERROR修改成功后 java -version 會產生如下變化:
兩者區別在于:當 JVM 運行在 -client 模式的時候,使用的是一個代號為 C1 的輕量級編譯器;而 -server 模式啟動的虛擬機采用相對更重量級的?C2 的編譯器。C2 比 C1 編譯器編譯得相對徹底。雖然這會導致程序啟動慢,但服務起來之后性能更高,同時有可能帶來可見性問題。
將上述代碼運行的匯編代碼打印出來,打印方法也簡單提一下。給主類運行時加上 VM Options:
-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly
此時會提示:
Could not load hsdis-i386.dll; library not loadable; PrintAssembly is disabled
因為打印匯編需要給 JDK 安裝一個插件,可能需要自己編譯 hsdis。不同平臺不太一樣:Windows 下32位 JDK 需要的是 hsdis-i386.dll;64位 JDK 需要 hsdis-amd64.dll。把編譯好的 hsdis-i386.dll 放到 JAVA_HOME/jre/bin/server 以及?JAVA_HOME/jre/bin/client 目錄中運行代碼。控制臺會把代碼對應的匯編指令一起打印出來。
輸出會有很多行,只需要搜索 run 方法對應的匯編。搜索 'run' '()V' in 'VisibilityTest'?可以找到對應的指令。如下面的代碼所示,從第26、27行注釋的部分可以看出:只有第一次進入循環之前檢查了下 stop 的值;不滿足條件進入循環后,不再檢查 stop, 一直在做循環 i++。
public void run() {int i = 0;while (!stop) {i++;}System.out.println("finish loop,i=" + i);}# {method} 'run' '()V' in 'VisibilityTest'......0x02d486e9: jne 0x02d48715// 獲取stop的值0x02d486eb: movzbl 0x64(%ebp),%ecx ; implicit exception: dispatches to 0x02d487030x02d486ef: test %ecx,%ecx// 進入while之前, 若stop滿足條件, 則跳轉到0x02d48703, 不執行while循環0x02d486f1: jne 0x02d48703 ;*goto; - VisibilityTest::run@12 (line 10)// 循環體內, i++0x02d486f3: inc %edi ; OopMap{ebp=Oop off=52};*goto; - VisibilityTest::run@12 (line 10)0x02d486f4: test %edi,0xe00000 ;*goto; - VisibilityTest::run@12 (line 10); {poll}// jmp, 無條件跳轉到0x02d486f3, 一直執行i++操作, 根本不檢查stop的值// 導致死循環0x02d486fa: jmp 0x02d486f30x02d486fc: mov $0x0,%ebp0x02d48701: jmp 0x02d486eb// 跳出循環0x02d48703: mov $0xffffff86,%ecx......解決方案也很簡單,只要給 stop 加上 volatile 關鍵字。再次輸出匯編代碼,發現每次都會檢查 stop 值,不再出現無限循環了。
// 給stop加上volatile后 public?void?run()?{int i = 0;while (!stop) {i++;}System.out.println("finish loop,i=" + i); } # {method} 'run' '()V' in 'VisibilityTest' ...... 0x02b4895c: mov 0x4(%ebp),%ecx ; implicit exception: dispatches to 0x02b4899d 0x02b4895f: cmp $0x5dd5238,%ecx ; {oop('VisibilityTest')} // 進入while判斷 0x02b48965: jne 0x02b4898d ;*aload_0; - VisibilityTest::run@2 (line 9) // 跳轉到0x02b48977獲取stop 0x02b48967: jmp 0x02b48977 0x02b48969: nopl 0x0(%eax) // 循環體內, i++ 0x02b48970:?inc????%ebx?????????????;?OopMap{ebp=Oop?off=49};*goto; - VisibilityTest::run@12 (line 10) 0x02b48971:?test???%edi,0xb30000????;*aload_0; - VisibilityTest::run@2 (line 9); {poll} // 循環過程中獲取stop的值 0x02b48977:?movzbl?0x64(%ebp),%eax??;*getfield?stop; - VisibilityTest::run@3 (line 9) // 驗證stop的值 0x02b4897b: test %eax,%eax // 若stop不符合條件, 則繼續跳轉到0x02b48970: inc, 執行i++, 否則中斷循環 0x02b4897d:?je?????0x02b48970???????;*ifne; - VisibilityTest::run@6 (line 9) 0x02b4897f: mov $0x33,%ecx 0x02b48984: mov %ebx,%ebp 0x02b48986: nop // 跳出循環, 執行System.out.print打印 0x02b48987:?call???0x02b2cac0???????;?OopMap{off=76};*getstatic out; - VisibilityTest::run@15 (line 12); {runtime_call} 0x02b4898c: int3 0x02b4898d: mov $0xffffff9d,%ecx ......再來看兩個?Java 語言規范中的例子,同樣涉及到編譯器優化重排。這里不再做詳細解釋,只介紹結果:例子1中有可能出現 r2 = 2 并且 r1 = 1 的情況。
例子2中是 r2,r5 值因為都等于?r1.x,編譯器會使用向前替換,把 r5 指向到 r2。最終可能導致 r2=r5=0,r4 = 3;
Happen-Before 先行發生規則
如果光靠 sychronized 和 volatile 來保證程序執行過程中的原子性、有序性、可見性,那么代碼將會變得異常繁瑣。JMM 提供了 Happen-Before 規則來約束數據之間是否存在競爭,線程環境是否安全。具體如下:
順序原則
一個線程內保證語義的串行性:a = 1; b = a + 1;
volatile 規則
volatile 變量的寫先發生于讀,從而保證了 volatile 變量的可見性。
鎖規則
解鎖(unlock)必然發生在隨后的加鎖(lock)前。
傳遞性
A 先于 B,B 先于 C,那么 A 必然先于 C。
線程啟動、中斷、終止
線程的 start() 方法先于它的每一個動作;
線程的中斷 interrupt() 先于被中斷線程的代碼;
線程的所有操作先于線程的終結 Thread.join()。
對象終結
對象的構造函數執行結束先于 finalize() 方法。
最后,再附上我歷時三個月總結的?Java 面試 + Java 后端技術學習指南,筆者這幾年及春招的總結,github 1.4k star,拿去不謝!
下載方式1.?首先掃描下方二維碼2.?后臺回復「Java面試」即可獲取總結
以上是生活随笔為你收集整理的Java内存模型与指令重排的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 万字详解,JDK1.8的Lambda、S
- 下一篇: 再见 JDK ...