什麼是 Java 內存模型?
致使可見性的緣由是
緩存,致使有序性的緣由是
編譯優化,那解決可見性、
有序性最直接的辦法就是禁用緩存和編譯優化,可是這樣問題雖然解決了,咱們程序的性能可就堪憂了。
合理的方案應該是按需禁用緩存以及編譯優化。那麼,如何作到「按需禁用」呢?對於併發程序,什麼時候禁用緩存以及編譯優化只有程序員知道,那所謂「按需禁用」其實就是指按照程序員的要求來禁用。因此,爲了解決可見性和有序性問題,只須要提供給程序員按需禁用緩存和編譯優化的方法便可。
Java 內存模型是個很複雜的規範,能夠從不一樣的視角來解讀,站在咱們這些程序員的視角,本質上能夠理解爲,
Java 內存模型規範了 JVM 如何提供按需禁用緩存和編譯優化的方法。具體來講,這些方法包括 volatile、synchronized 和 final 三個關鍵字,以及六項 Happens-Before 規則。
volatile
禁用 CPU 緩存
Happens-Before 規則
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
// 這裏 x 會是多少呢?
}
}
}
1. 程序的順序性規則
程序前面對某個變量的修改必定是對後續操做可見的。
2. volatile 變量規則
這條規則是指對一個 volatile 變量的寫操做, Happens-Before 於後續對這個 volatile變量的讀操做。
3. 傳遞性
這條規則是指若是 A Happens-Before B,且 B Happens-Before C,那麼 A Happens-Before C。
從圖中,咱們能夠看到:
1. 「x=42」 Happens-Before 寫變量 「v=true」 ,這是規則 1 的內容;
2. 寫變量「v=true」 Happens-Before 讀變量 「v=true」,這是規則 2 的內容 。
再根據這個傳遞性規則,咱們獲得結果:「x=42」 Happens-Before 讀變量「v=true」。這意味着什麼呢?
若是線程 B 讀到了「v=true」,那麼線程 A 設置的「x=42」對線程 B 是可見的。也就是說,線程 B 能看到 「x == 42」 ,有沒有一種恍然大悟的感受?這就是 1.5 版本對volatile 語義的加強
4. 管程中鎖的規則
這條規則是指對一個鎖的解鎖 Happens-Before 於後續對這個鎖的加鎖。要理解這個規則,就首先要了解「管程指的是什麼」。管程是一種通用的同步原語,在Java 中指的就是 synchronized,synchronized 是 Java 裏對管程的實現。管程中的鎖在 Java 裏是隱式實現的,例以下面的代碼,在進入同步塊以前,會自動加鎖,而在代碼塊執行完會自動釋放鎖,加鎖以及釋放鎖都是編譯器幫咱們實現的。
synchronized (this) { // 此處自動加鎖
// x 是共享變量, 初始值 =10
if (this.x < 12) {
this.x = 12;
}
} // 此處自動解鎖
因此結合規則 4——管程中鎖的規則,能夠這樣理解:假設 x 的初始值是 10,線程 A 執行完代碼塊後 x 的值會變成 12(執行完自動釋放鎖),線程 B 進入代碼塊時,可以看到線程 A 對 x 的寫操做,也就是線程 B 可以看到 x==12。
5. 線程 start() 規則
這條是關於線程啓動的。它是指主線程 A 啓動子線程 B 後,子線程 B 可以看到主線程在啓動子線程 B 前的操做。換句話說就是,若是線程 A 調用線程 B 的 start() 方法(即在線程 A 中啓動線程 B),那
麼該 start() 操做 Happens-Before 於線程 B 中的任意操做。具體可參考下面示例代碼。
Thread B = new Thread(() -> {
// 主線程調用 B.start() 以前
// 全部對共享變量的修改,此處皆可見
// 此例中,var==77
});
// 此處對共享變量 var 修改
var = 77;
// 主線程啓動子線程
B.start();
6. 線程 join() 規則
這條是關於線程等待的。它是指主線程 A 等待子線程 B 完成(主線程 A 經過調用子線程B 的 join() 方法實現),當子線程 B 完成後(主線程 A 中 join() 方法返回),主線程能 夠看到子線程的操做。固然所謂的「看到」,指的是對共享變量的操做。換句話說就是,若是在線程 A 中,調用線程 B 的 join() 併成功返回,那麼線程 B 中的任意操做 Happens-Before 於該 join() 操做的返回。具體可參考下面示例代碼。
Thread B = new Thread(()->{
// 此處對共享變量 var 修改
var = 66;
});
// 例如此處對共享變量修改,
// 則這個修改結果對線程 B 可見
// 主線程啓動子線程
B.start();
B.join()
// 子線程全部對共享變量的修改
// 在主線程調用 B.join() 以後皆可見
// 此例中,var==66
被咱們忽視的 final
前面咱們講 volatile 爲的是禁用緩存以及編譯優化,咱們再從另一個方面來看,有沒有辦法告訴編譯器優化得更好一點呢?這個能夠有,就是final 關鍵字。
final 修飾變量時,初衷是告訴編譯器:這個變量生而不變,能夠可勁兒優化。