以前咱們說了:
1,可見性
2,原子性
3,有序性
3個併發BUG的之源,這三個也是編程領域的共性問題。Java誕生之處就支持多線程,因此天然有解決這些問題的辦法,並且在編程語言領域處於領先地位。理解Java解決併發問題的方案,對於其餘語言的解決方案也有舉一反三的效果。html
咱們已經知道了,致使可見性的緣由是緩存,致使有序性的問題是編譯優化。那解決問題的辦法就是直接禁用 緩存和編譯優化
。可是直接不去使用這些是不行了,性能沒法提高。
因此合理的方案是 按需禁用緩存和編譯優化。如何作到「按需禁用」
,只有編寫代碼的程序員本身知道,因此程序須要給程序員按需禁用和編譯優化
的方法才行。java
Java的內存模型若是站在程序員的角度,能夠理解爲,Java內存模型規範了JVM如何提供按需禁用緩存和編譯優化的方法。具體來講,這些方法包括volatile
,synchronized
和final
三個關鍵字段。
以及六項 Happens-Before
規則。程序員
volatile
關鍵字並非 Java 語言特有的,C語言也有,它的原始意義就是禁用CPU緩存。編程
例如,咱們聲明一個volatile
變量 ,volatile int x = 0
,它表達的是:告訴編譯器,對這個變量的讀寫,不能使用 CPU 緩存,必須從內存中讀取或者寫入。看起來語義很明確,實際狀況比較困惑。緩存
看下如下代碼:多線程
class VolatileExample { int x = 0; volatile boolean v = false; public void writer() { x = 42; v = true; } public void reader() { if (v == true) { // 這裏 x 會是多少呢? } } }
直覺上看,這裏的X應該是42,那實際應該是多少呢?這個要看Java的版本,若是在低於 1.5 版本上運行,x 多是42,也有多是 0;若是在 1.5 以上的版本上運行,x 就是等於 42。
分析一下,爲何 1.5 之前的版本會出現 x = 0 的狀況呢?由於變量 x 可能被 CPU 緩存而致使可見性問題。這個問題在 1.5 版本已經被圓滿解決了。Java 內存模型在 1.5 版本對 volatile 語義進行了加強。怎麼加強的呢?答案是一項 Happens-Before
規則。併發
這裏直接給出定義:app
Happens-Before :前面一個操做的結果對後續操做是可見的。
再進一步的講:Happens-Before 約束了編譯器的優化行爲,雖容許編譯器優化,可是要求編譯器優化後必定遵照 Happens-Before 規則。編程語言
看一看Java內存模型定義了哪些重要的Happens-Before
規則函數
1,程序的順序性規則
這條規則是指在一個線程中,按照程序順序,前面的操做 Happens-Before
於後續的任意操做。好比剛纔那段示例代碼,按照程序的順序,第 6 行代碼 x = 42;
Happens-Before 於第 7 行代碼 v = true;
,這就是規則 1 的內容,也比較符合單線程裏面的思惟:程序前面對某個變量的修改必定是對後續操做可見的。
2,volatile 變量規則
這條規則是指對一個 volatile 變量的寫操做,Happens-Before
於後續對這個 volatile 變量的讀操做。
這個就有點費解了,對一個 volatile 變量的寫操做相對於後續對這個 volatile 變量的讀操做可見。
3,傳遞性
這條規則是指若是 A Happens-Before B,且 B Happens-Before C,那麼 A Happens-Before C。
咱們將規則 3 的傳遞性應用到咱們的例子中,能夠看下面這幅圖:
class VolatileExample { int x = 0; volatile boolean v = false; public void writer() { x = 42; v = true; } public void reader() { if (v == true) { // 這裏 x 會是多少呢? } } }
從圖中能夠看到
1,x=42 Happens-Before
寫 v=true,這是規則1
2,讀v=true Happens-Before
讀變量X,這是規則2
結合傳遞性讀定義,即:
線程A的 x=42
Happens-Before
線程B的 讀變量X
java 1.5對 volatile 的加強就是這個,根據這個定義就保證了以前的 x=42
的成立
4,管程中鎖的規則
這條規則是指對一個鎖的解鎖 Happens-Before 於後續對這個鎖的加鎖。
管程 (英語:Moniters,也稱爲監視器) 是一種程序結構,結構內的多個子程序(對象或模塊)造成的多個工做線程互斥訪問共享資源。
管程
在 Java 中指的就是 synchronized,synchronized 是 Java 裏對管程的實現。管程
中的鎖在 Java 裏是隱式實現的,例以下面的代碼,在進入同步塊以前,會自動加鎖,而在代碼塊執行完會自動釋放鎖,加鎖以及釋放鎖都是編譯器幫咱們實現的。
synchronized (this) { // 此處自動加鎖 // x 是共享變量, 初始值 =10 if (this.x < 12) { this.x = 12; } } // 此處自動解鎖
因此結合規則定義,能夠這樣理解:假設 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
前面咱們講 volatile 爲的是禁用緩存以及編譯優化,那 final關鍵字 就是告訴編譯器優化得更好一點。
final 修飾變量時,初衷是告訴編譯器:這個變量生而不變,能夠儘可能優化。可是Java編譯器在 1.5 之前的版本致使優化錯誤了。
構造函數的錯誤重排致使線程可能看到 final 變量的值會變化。詳細的案例能夠參考:http://www.cs.umd.edu/~pugh/j...
固然了,在 1.5 之後 Java 內存模型對 final
類型變量的重排進行了約束。如今只要咱們提供正確構造函數沒有「逸出」,就不會出問題了。
在下面例子中,在構造函數裏面將 this 賦值給了全局變量 global.obj,這就是「逸出」,線程經過 global.obj 讀取 x 是有可能讀到 0 的。所以咱們必定要避免「逸出」。
final int x; // 錯誤的構造函數 public FinalFieldExample() { x = 3; y = 4; // 此處就是講 this 逸出, global.obj = this; }
Java 的內存模型是併發編程領域的一次重要創新,Happens-Before 的語義是一種因果關係。在現實世界裏,若是 A 事件是致使 B 事件的原由,那麼 A 事件必定是先於(Happens-Before)B 事件發生的,這個就是 Happens-Before 語義的現實理解。
在 Java 語言裏面,Happens-Before 的語義本質上是一種可見性,A Happens-Before B 意味着 A 事件對 B 事件來講是可見的,不管 A 事件和 B 事件是否發生在同一個線程裏。例如 A 事件發生在線程 1 上,B 事件發生在線程 2 上,Happens-Before 規則保證線程 2 上也能看到 A 事件的發生。
Java 內存模型主要分爲兩部分,一部分面向你我這種編寫併發程序的應用開發人員,另外一部分是面向 JVM 的實現人員的,咱們能夠重點關注前者,也就是和編寫併發程序相關的部分,這部份內容的核心就是 Happens-Before 規則。
參考:
Java內存模型