若是熟悉Java併發編程的話,應該知道在多線程共享變量的狀況下,存在「內存可見性問題」:html
在一個線程中對某個變量進行賦值,而後在另一個線程中讀取該變量的值,讀取到的可能仍然是之前的值;編程
這裏並不是說的是時序的問題,即便在另一個線程中循環讀取該變量的值,也可能永遠讀不到該變量的最新值。緩存
請看下面這段代碼:多線程
1 public class Main extends Thread { 2 private static boolean flag = false; 3 4 @Override 5 public void run() { 6 while (!flag) { 7 //System.out.flush(); 8 } 9 } 10 11 public static void main(String[] args) { 12 Main m = new Main(); 13 m.start(); 14 try { 15 Thread.sleep(200); 16 } catch (InterruptedException e) { 17 e.printStackTrace(); 18 } 19 flag = true; 20 try { 21 m.join(); 22 } catch (InterruptedException e) { 23 e.printStackTrace(); 24 } 25 System.out.println("done"); 26 } 27 }
這段代碼在Windows(Java 7 HotSpot),Linux(Java 7 OpenJDK),MacOS(Java 7 HotSpot)上運行的時候根本停不下開;然而在Android(Dalvik)上,相似的代碼則能夠正常結束;咱們知道,若是將變量flag聲明爲volatile的話,那麼這段代碼無論在哪一個平臺上運行均可以正常結束,事實也確實如此;這些平臺都沒有問題,它們的行爲都符合JMM規範,只不過Android(Dalvik)的行爲更保守一些而已。併發
疑惑在於,爲何是「永遠不可見」?我以前一直覺得「內存可見性問題」只是時間長短而已。ide
更詭異的是,若是將while循環中的System.out.flush()打開的話,程序又均可以正常結束了,這又是什麼緣由呢?post
首先,咱們從字節碼入手,發現它們對應的字節碼基本上是同樣的;即便是volatile版本,也只不過是在變量上增長了一個volatile標記,字節碼並沒有不一樣。性能
據此,咱們能夠推斷,差別可能來源於JIT,因而關掉JIT(如何控制JVM中的JIT行爲?),果真,這些代碼又均可以正常結束了。學習
按照我以前學習到的一些有關多核CPU方面的知識,多核CPU的行爲並不會致使「永遠不可見」的問題,理由以下:優化
1.若是是CPU緩存,多核CPU之間存在「緩存一致性」協議,因此這裏並不會致使「不可見」的問題;
2.若是是CPU Store Buffer,由於容量有限,早晚會寫回到緩存,因此這裏並不會致使「永遠不可見」的問題;
3.若是是CPU指令重排序,因爲這段代碼是在一個循環中讀取變量的值,因此這裏不會有任何影響。
那麼,問題就只能出在JIT生成的代碼上了,讓咱們查看一下JIT生成的代碼(如何控制JVM中的JIT行爲?):
這個是無volatile無System.out.flush()的版本,它不能中止,說明以下:
第一個紅色標記,讀取flag的值
第二個紅色標記,判斷flag的值是否爲false,若是是則順序執行到第三個紅色標記處
第三個紅色標記,這裏是一個死循環
從這裏能夠看出,JIT對生成的代碼作了高度優化,它認爲代碼中沒有地方對flag進行修改,所以直接生成一段死循環代碼,避免反覆讀取flag的值以提高性能,可是這違背了這段代碼的原意,致使程序不能中止。
這個是有volatile的版本,它能夠正常結束,說明以下:
第一個紅色標記,讀取flag的值
第二個紅色標記,判斷flag的值是否爲false,若是是則跳轉到第個紅色標記處
這徹底符合這段代碼的原意,所以能夠正常結束。
這個是有System.out.flush()的版本,從紅色標記處能夠看出,這裏也徹底符合代碼原意,所以能夠正常結束;因爲某種緣由,JIT沒有對生成的代碼進行優化。
至此,疑惑已徹底解開,在此也順便總結一下Java中的volatile關鍵字:
1.阻止Java編譯器對字節碼進行重排序(彷佛沒有Java實如今字節碼層面進行重排序)
2.在JIT生成的代碼中插入適當的內存屏障指令
3.禁止JIT過分優化生成的代碼
3.字節碼層面並不會關心volatile(變量標記除外),執行引擎和JIT應該關心