且看一段測試代碼, 在不借助外界工具的條件下得出你本身的答案。java
import java.util.*;
import java.util.concurrent.CountDownLatch;
public class Reordering {
static int a = 0;
static int b = 0;
static int x = 0;
static int y = 0;
static final Set<Map<Integer, Integer>> ans = new HashSet<>(4);
public void help() throws InterruptedException {
final CountDownLatch latch = new CountDownLatch(2);
Thread threadOne = new Thread(() -> {
a = 1;
x = b;
latch.countDown();
});
Thread threadTwo = new Thread(() -> {
b = 1;
y = a;
latch.countDown();
});
threadOne.start();
threadTwo.start();
latch.await();
Map<Integer, Integer> map = new HashMap<>();
map.put(x, y);
if (!ans.contains(map)) {
ans.add(map);
}
}
@Test
public void testReordering() throws InterruptedException {
for (int i = 0; i < 20000 && ans.size() != 4; i++) {
help();
a = x = b = y = 0;
}
help();
System.out.println(ans);
}
}
複製代碼
你的結果ans多是[{0=>1}, {1=>1}, {1=>0}]
, 由於線程調度是隨機的, 有可能一個線程執行了, 另一個線程纔得到cpu的執行權, 又或者是兩個線程交疊執行, 這種狀況下ans的答案無疑是上面三種結果, 至於上面三種結果對應的線程執行順序, 我這裏就不模擬了, 這不是重點。可是其實ans除了上面的三種結果以外, 還有另一種結果{0=>0}
, 這是爲何呢? 要想出現{0=>0}這種結果無非就是:安全
x = b happens before a = 1
呢? 這其實就是指令重排序。大多數現代微處理器都會採用將指令亂序執行的方法, 在條件容許的狀況下, 直接運行當前有能力當即執行的後續指令, 避開獲取下一條指令所需數據時形成的等待。經過亂序執行的技術, 處理器能夠大大提升執行效率。除了cpu會對指令重排序來優化性能以外, java JIT也會對指令進行重排序。app
那麼何時不由止指令重排序或者怎麼禁止指令重排序呢?否則一切都亂套了。函數
其一, 有數據依賴關係的指令不會進行指令重排序! 什麼意思呢?工具
a = 1;
x = a;
複製代碼
就像上面兩條指令, x
依賴於a
, 因此x = a
這條指令不會重排序到a = 1
這條指令的前面。性能
有數據依賴關係分爲如下三種:測試
a = 1
和x = a
, 這就是典型的寫後讀, 這種不會進行指令重排序。a = 1
和a = 2
, 這種也不會進行重排序。x = a
和a = 1
。什麼是as-if-serial? as-if-serial語義就是: 無論怎麼重排序(編譯器和處理器爲了提升並行度), 單線程程序的執行結果不能被改變。因此編譯器和cpu進行指令重排序時候回遵照as-if-serial語義。舉個栗子:優化
x = 1; //1
y = 1; //2
ans = x + y; //3
複製代碼
上面三條指令, 指令1和指令2沒有數據依賴關係, 指令3依賴指令1和指令2。根據上面咱們講的重排序不會改變咱們的數據依賴關係, 依據這個結論, 咱們能夠確信指令3是不會重排序於指令1和指令2的前面。咱們看一下上面上條指令編譯成字節碼文件以後:this
public int add() {
int x = 1;
int y = 1;
int ans = x + y;
return ans
}
複製代碼
對應的字節碼spa
public int add();
Code:
0: iconst_1 // 將int型數值1入操做數棧
1: istore_1 // 將操做數棧頂數值寫到局部變量表的第2個變量(由於非靜態方法會傳入this, this就是第一個變量)
2: iconst_1 // 將int型數值1入操做數棧
3: istore_2 // 將將操做數棧頂數值寫到局部變量表的第3個變量
4: iload_1 // 將第2個變量的值入操做數棧
5: iload_2 // 將第三個變量的值入操做數棧
6: iadd // 操做數棧頂元素和棧頂下一個元素作int型add操做, 並將結果壓入棧
7: istore_3 // 將棧頂的數值存入第四個變量
8: iload_3 // 將第四個變量入棧
9: ireturn // 返回
複製代碼
以上的字節碼咱們只關心0->7行, 以上8行指令咱們能夠分爲:
上面的5個操做, 1操做和二、4可能會重排序, 2操做和一、3ch重排序, 操做3可能和二、4重排序, 操做4可能和一、3重排序。對應上面的賦值x和賦值y有可能會進行重排序, 對, 這並不難以理解, 由於寫x和寫y並無明確的數據依賴關係。可是操做1和3和5並不能重排序, 由於3依賴1, 5依賴3, 同理操做二、四、5也不能進行重排序。
因此爲了保證數據依賴性不被破壞, 重排序要遵照as-if-serial語義。
@Test
public void testReordering2() {
int x = 1;
try {
x = 2; //A
y = 2 / 0; //B
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(x);
}
}
複製代碼
上面這段代碼A和B是有可能重排序的, 由於x和y並無數據依賴關係, 而且也沒有特殊的語義作限制。可是若是發生B happens-before A的話, 此時是否是就打印了錯誤的x的值, 其實否則: 爲了保證as-if-serial語義, Java異常處理機制對重排序作了一種特殊的處理: JIT在重排序時會在catch語句中插入錯誤代償代碼(即重排序到B後面的A), 這樣作雖然會致使catch裏面的邏輯變得複雜, 可是JIT優化原則是: 儘量地優化程序正常運行下的邏輯, 哪怕以catch塊邏輯變得複雜爲代價。
這就是happens-before傳遞性
Java內存模型(Java Memory Model簡稱JMM)總結了如下8條規則, 保證符合如下8條規則, happens-before先後兩個操做, 不會被重排序且後者對前者的內存可見。
有人確定寫過下面的double-check單例模式
public class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
複製代碼
可是這種double-check加鎖的單例是正常的嗎? No. 由於建立一個實例對象並非一個原子性的操做, 並且還可能發生重排序, 具體以下: 假定建立一個對象須要:
上面的2和3操做是有可能重排序的, 若是3重排序到2的前面, 這時候2操做尚未執行, instance已經不是null了, 固然不是安全的。
那麼怎麼防止這種指令重排序? 修改以下:
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
複製代碼
volatile
關鍵字有兩個語義: 其一保證內存可見性, 這個語義咱們下次博客會講到(其實就是一個線程修改會對另外一個線程可見, 若是不是volatile
, 線程操做都是在TLAB有副本的, 修改了副本的值以後不即時刷新到主存, 其餘線程是不可見的) 其二, 禁止指令重排序, 若是上面new
的時候, 禁止了指令重排序, 因此能獲得指望的狀況。
題外話, 關於線程安全的單例, 每每能夠採用靜態內部類的形式來實現, 這種無疑是最合適的了。
public class Singleton {
public static Singleton getInstance() {
return Helper.instance;
}
static class Helper {
private static final Singleton instance = new Singleton();
}
}
複製代碼
咱們以前一會容許重排序, 一會禁止重排序, 可是重排序禁止是怎麼實現的呢? 是用內存屏障cpu指令來實現的, 顧名思義, 就是加個障礙, 不讓你重排序。
內存屏障能夠被分爲如下幾種類型: