首先貼一段測試程式碼:
public class TestMemoryBarrier {
boolean running = false;
boolean get() {
return running;
}
void doSetTrue() {
running = true;
}
public static void main(String[] args) throws InterruptedException {
TestMemoryBarrier instance = new TestMemoryBarrier();
new Thread(
() -> {
while (!instance.get()) {
}
System.out.println("Thread 1 finished.");
}).start();
Thread.sleep(100);
new Thread(
() -> {
instance.doSetTrue();
System.out.println("Thread 2 finished.");
}).start();
}
}
複製程式碼
首先啟動第一個執行緒執行緒1去迴圈獲取bool變數running
的值(預設是false
),如果running
變為true
後迴圈結束,執行緒終止。
再起第二個執行緒執行緒2去修改running
的值為true
,當修改完後輸出提示。
注意: 第一個執行緒啟動後主執行緒睡眠一段時間,確保第一個執行緒已經先於執行緒二獲取cpu時間片。
這裡結果輸出是:
Thread 2 finished.
複製程式碼
因為無法獲知執行緒2對共享變數running
做出的修改, 然後執行緒1一直處在執行狀態。
這裡簡單說明一下Java Mememory Model簡稱JMM:
在本例中執行緒2實際上是修改了自己本地記憶體中的running
值, 但是並沒有重新整理到主記憶體中,執行緒1也一直在讀自己本地記憶體中的值,並沒有去主記憶體中重新獲取。
為了讓例子最終能輸出
Thread 1 finished
複製程式碼
方法:很簡單, 直接設定變數running
為volatile,以保證其在多執行緒環境中的記憶體可見性問題。
本文的重點是對插入的記憶體屏障進行測試,所以以上只是開頭。
測試volatile插入的記憶體屏障指令,變更程式碼為:
新增一個類,包含一個volatile的變數並賦值。
在get()
方法return前, new一個這個新增類的例項物件,完整程式碼:
class VolatileClass {
private volatile int a = 1;
}
public class TestMemoryBarrier {
boolean running = false;
boolean get() {
VolatileClass aClass = new VolatileClass();
return running;
}
void doSetTrue() {
running = true;
}
public static void main(String[] args) throws InterruptedException {
TestMemoryBarrier instance = new TestMemoryBarrier();
new Thread(
() -> {
while (!instance.get()) {
}
System.out.println("Thread 1 finished.");
}).start();
Thread.sleep(100);
new Thread(
() -> {
instance.doSetTrue();
System.out.println("Thread 2 finished.");
}).start();
}
}
複製程式碼
這裡同樣能使執行緒1結束。原因在於新建例項的時候對volatile變數進行了讀寫操作
volatile插入的記憶體屏障指令如下圖:(這裡不拉出重排序相關話題)
總結:在進行volatile變數寫的時候,StoreStore確保前面執行緒2修改的普通變數已經被flush到主記憶體中了。
測試synchronized關鍵字對可見性的影響:
為了套用Happen-Before規則,這裡直接在get()
和doSetTrue()
方法上加synchronized
也能保證可見性問題。但這裡假設只在get()
方法上加同步呢? 結果是執行緒1也能獲取最新值。
JMM關於synchronized的兩條規定:
- 執行緒解鎖前,必須把共享變數的最新值重新整理到主記憶體中
- 執行緒加鎖時,將清空工作記憶體中共享變數的值,從而使用共享變數時需要從主記憶體中重新讀取最新的值
如果只是在doSetTrue()
方法上加鎖,執行緒1並不會獲取最新的值,原因是:雖然執行緒2已經將修改過的變數重新整理到主存了,但是get()
方法並不會去從主存讀取最新的值。
以上例子可能不符合happen-before規則,只是探討結果出現的原因。
如果文章中出現對某處有錯誤理解的地方,歡迎大家指出。
參考文章:
- The JSR-133 Cookbook for Compiler Writers
- 深入理解Java記憶體模型(程曉明)