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


如果熟悉Java並發編程的話,應該知道在多線程共享變量的情況下,存在“內存可見性問題”:

在一個線程中對某個變量進行賦值,然后在另外一個線程中讀取該變量的值,讀取到的可能仍然是以前的值;

這里並非說的是時序的問題,即使在另外一個線程中循環讀取該變量的值,也可能永遠讀不到該變量的最新值。

請看下面這段代碼:

 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)的行為更保守一些而已。

疑惑在於,為什么是“永遠不可見”?我之前一直以為“內存可見性問題”只是時間長短而已。

更詭異的是,如果將while循環中的System.out.flush()打開的話,程序又都可以正常結束了,這又是什么原因呢?

首先,我們從字節碼入手,發現它們對應的字節碼基本上是一樣的;即使是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應該關心


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM