一個Java內存可見性問題的分析

若是熟悉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應該關心

相關文章
相關標籤/搜索