本文暫不講JMM(Java Memory Model)中的主存, 工做內存以及數據如何在其中流轉等等,java
這些自己還牽扯到硬件內存架構, 直接上手容易繞暈, 先從如下幾個點探索JMM緩存
原子性是指一個操做是不可中斷的. 即便是在多個線程一塊兒執行的時候,安全
一個操做一旦開始,就不會被其它線程干擾. 例如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可能會對指令從新排序.
一條彙編指令的執行是能夠分爲不少步驟的, 分爲不一樣的硬件執行
既然指令能夠被分解爲不少步驟, 那麼多條指令就不必定依次序執行.
由於每次只執行一條指令, 依次執行效率過低了, 假設上述每個步驟都要消耗一個時鐘週期,
那麼依次執行的話, 一條指令要5個時鐘週期, 兩條指令要佔用10個時鐘週期, 三條指令消耗15個時鐘.
而若是硬件空閒便可執行下一步, 相似於工廠中的流水線, 一條指令要5個時鐘週期,
兩條指令只須要6個時鐘週期, 由於是錯位流水執行, 三條指令消耗7個時鐘.
舉個例子 A = B + C, 須要以下指令
注意下圖紅色框選部分, 指令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;
若是光靠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()方法.