Java內存模型與指令重排

本文暫不講JMM(Java Memory Model)中的主存, 工做內存以及數據如何在其中流轉等等,java

這些自己還牽扯到硬件內存架構, 直接上手容易繞暈, 先從如下幾個點探索JMM緩存

  • 原子性
  • 有序性
  • 可見性
  • 指令重排
    • CPU指令重排
    • 編譯器優化重排
  • Happen-Before規則

原子性

原子性是指一個操做是不可中斷的. 即便是在多個線程一塊兒執行的時候,安全

一個操做一旦開始,就不會被其它線程干擾. 例如CPU中的一些指令, 屬於原子性的,多線程

又或者變量直接賦值操做(i = 1), 也是原子性的, 即便有多個線程對i賦值, 相互也不會干擾.架構

而如i++, 則不是原子性的, 由於他實際上i = i + 1, 若存在多個線程操做i, 結果將不可預期.app

有序性

有序性是指在單線程環境中, 程序是按序依次執行的.jvm

而在多線程環境中, 程序的執行可能由於指令重排而出現亂序, 下文會有詳細講述.函數

 1     class OrderExample {
 2         int a = 0;
 3         boolean flag = false;
 4 
 5         public void writer() {
 6             // 如下兩句執行順序可能會在指令重排等場景下發生變化
 7             a = 1;
 8             flag = true;
 9         }
10 
11         public void reader() {
12             if (flag) {
13                 int i = a + 1;
14                 ……
15             }
16         }
17     }

 

可見性

可見性是指當一個線程修改了某一個共享變量的值,其餘線程是否可以當即知道這個修改.oop

會有多種場景影響到可見性:性能

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模式下運行, 永遠不會中止.

 1 /**
 2  * Created by Administrator on 2018/5/3/0003.
 3  */
 4 public class VisibilityTest extends Thread {
 5     private boolean stop;
 6 
 7     public void run() {
 8         int i = 0;
 9         while (!stop) {
10             i++;
11        }
12         System.out.println("finish loop,i=" + i);
13     }
14 
15     public void stopIt() {
16         stop = true;
17     }
18 
19     public boolean getStop() {
20          return stop;
21     }
22  
23      public static void main(String[] args) throws Exception {
24          VisibilityTest v = new VisibilityTest();
25          v.start();
26          Thread.sleep(1000);
27         v.stopIt();
28          Thread.sleep(2000);
29          System.out.println("finish main");
30          System.out.println(v.getStop());
31     }
32  }

以32位jdk1.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', 能夠找到對應的指令.

以下代碼所示, 從紅字註釋的部分能夠看出來, 

只有第一次進入循環以前, 檢查了下stop的值, 不知足條件進入循環後, 

再也沒有檢查stop, 一直在作循環i++.

 1      public void run() {
 2         int i = 0;
 3         while (!stop) {
 4             i++;
 5         }
 6         System.out.println("finish loop,i=" + i);
 7     }
 8   
 9   
10   # {method} 'run' '()V' in 'VisibilityTest'
11   ......
12   0x02d486e9: jne    0x02d48715
13   // 獲取stop的值
14   0x02d486eb: movzbl 0x64(%ebp),%ecx    ; implicit exception: dispatches to 0x02d48703
15   0x02d486ef: test   %ecx,%ecx
16   // 進入while以前, 若stop知足條件, 則跳轉到0x02d48703, 不執行while循環
17   0x02d486f1: jne    0x02d48703         ;*goto
18                                         ; - VisibilityTest::run@12 (line 10)
19   // 循環體內, i++
20   0x02d486f3: inc    %edi               ; OopMap{ebp=Oop off=52}
21                                         ;*goto
22                                         ; - VisibilityTest::run@12 (line 10)
23   0x02d486f4: test   %edi,0xe00000      ;*goto
24                                         ; - VisibilityTest::run@12 (line 10)
25                                         ;   {poll}
26   // jmp, 無條件跳轉到0x02d486f3, 一直執行i++操做, 根本不檢查stop的值 27   // 致使死循環 28   0x02d486fa: jmp    0x02d486f3
29   0x02d486fc: mov    $0x0,%ebp
30   0x02d48701: jmp    0x02d486eb
31   // 跳出循環
32   0x02d48703: mov    $0xffffff86,%ecx
33   ......

解決方案也很簡單, 只要給stop加上volatile關鍵字, 再次打印彙編代碼, 發現他每次都會檢查stop的值.

就不會出現無限循環了.

 1     // 給stop加上volatile後
 2     public void run() {
 3         int i = 0;
 4         while (!stop) {
 5             i++;
 6         }
 7         System.out.println("finish loop,i=" + i);
 8     }
 9 
10   # {method} 'run' '()V' in 'VisibilityTest'
11   ......
12   0x02b4895c: mov    0x4(%ebp),%ecx     ; implicit exception: dispatches to 0x02b4899d
13   0x02b4895f: cmp    $0x5dd5238,%ecx    ;   {oop('VisibilityTest')}
14   // 進入while判斷
15   0x02b48965: jne    0x02b4898d         ;*aload_0
16                                         ; - VisibilityTest::run@2 (line 9)
17   // 跳轉到0x02b48977獲取stop
18   0x02b48967: jmp    0x02b48977
19   0x02b48969: nopl   0x0(%eax)
   // 循環體內, i++
20 0x02b48970: inc %ebx ; OopMap{ebp=Oop off=49} 21 ;*goto 22 ; - VisibilityTest::run@12 (line 10) 23 0x02b48971: test %edi,0xb30000 ;*aload_0 24 ; - VisibilityTest::run@2 (line 9) 25 ; {poll} 26 // 循環過程當中獲取stop的值 27 0x02b48977: movzbl 0x64(%ebp),%eax ;*getfield stop 28 ; - VisibilityTest::run@3 (line 9) 29 // 驗證stop的值 30 0x02b4897b: test %eax,%eax 31 // 若stop不符合條件, 則繼續跳轉到0x02b48970: inc, 執行i++, 不然中斷循環 32 0x02b4897d: je 0x02b48970 ;*ifne 33 ; - VisibilityTest::run@6 (line 9) 34 0x02b4897f: mov $0x33,%ecx 35 0x02b48984: mov %ebx,%ebp 36 0x02b48986: nop 37 // 跳出循環, 執行System.out.print打印 38 0x02b48987: call 0x02b2cac0 ; OopMap{off=76} 39 ;*getstatic out 40 ; - VisibilityTest::run@15 (line 12) 41 ; {runtime_call} 42 0x02b4898c: int3 44 0x02b4898d: mov $0xffffff9d,%ecx 45 ......

 

再來看兩個從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()方法.

相關文章
相關標籤/搜索