[Java併發-2]Java如何解決可見性問題的

以前咱們說了:
1,可見性
2,原子性
3,有序性
3個併發BUG的之源,這三個也是編程領域的共性問題。Java誕生之處就支持多線程,因此天然有解決這些問題的辦法,並且在編程語言領域處於領先地位。理解Java解決併發問題的方案,對於其餘語言的解決方案也有舉一反三的效果。html

什麼是Java內存模型

咱們已經知道了,致使可見性的緣由是緩存,致使有序性的問題是編譯優化。那解決問題的辦法就是直接禁用 緩存和編譯優化。可是直接不去使用這些是不行了,性能沒法提高。
因此合理的方案是 按需禁用緩存和編譯優化。如何作到「按需禁用」,只有編寫代碼的程序員本身知道,因此程序須要給程序員按需禁用和編譯優化的方法才行。java

Java的內存模型若是站在程序員的角度,能夠理解爲,Java內存模型規範了JVM如何提供按需禁用緩存和編譯優化的方法。具體來講,這些方法包括volatile,synchronizedfinal三個關鍵字段。
以及六項 Happens-Before 規則。程序員

使用volatile的困惑

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 規則。併發

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

過分優化的 final

前面咱們講 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內存模型

相關文章
相關標籤/搜索