JMM 必知必會

做者簡介html

隆基,你們都喜歡叫他大帝,風度恰恰,開保時捷的男子。擅長 Java 和 Go ,學習能力超強、工做能力超強。java

什麼是JAVA內存模型

由於CPU處理的速度比內存讀取的速度快不少,經過緩存能夠極大的提高CPU處理速度。而且,多級緩存的設計,能夠平衡緩存大小與芯片體積、成本,在現代CPU中普遍使用。在多核且多級緩存的條件下,若是多個核同時讀寫內存的同一行,如何保證數據的一致性?程序員

在處理器級別,內存模型定義了什麼條件下該核可以看到其餘核的寫入和該核的寫入可以對其餘核可見。有如下兩種模型:數組

  1. 強一致內存模型,即任什麼時候間任何核的寫入都對其餘核可見
  2. 弱一致內存模型,即經過一些特殊的內存屏障指令(Memory Barrier),來刷新內存或者失效本地核處理器緩存,來保證核間的可見性

如今弱一致內存模型愈來愈流行,由於對一致性的弱化爲CPU的性能優化提供了更大的空間。緩存

除了緩存的問題,編譯器對 代碼的重排序 更加加劇了一致性問題。只要沒有改變程序的語義,編譯器能夠自由的調整代碼的執行順序,提早或延後代碼的執行,因此對內存的寫入也會提早或延後。在真正寫入前,其餘核是沒法看到對內存所作的讀寫的。性能優化

這不是Bug,設計就是這樣。 只要不違反內存模型的定義,編譯器、運行時、硬件均可以自由的去調整執行順序,來獲得最優的性能。多線程

舉個例子:架構

class Reordering {
  int x = 0, y = 0;
  public void writer() {
    x = 1;
    y = 2;
  }

  public void reader() {
    int r1 = y;
    int r2 = x;
  }
}
複製代碼

若是讀寫在兩個線程中併發執行,而且讀到了r1=2,那麼,r2=1嗎?併發

不必定,寫線程可能作了代碼重排序,若是執行順序以下:app

  1. 寫線程寫y=2
  2. 讀線程讀y=2,x=0
  3. 寫線程寫x=1

程序的執行結果是r1=2, r2=0

最後,引出咱們問題的答案,JAVA內存模型定義了在多線程環境下什麼樣的行爲是合法的,而且線程間是如何跟內存交互的。他描述了代碼變量跟內存、寄存器處理這些變量的底層實現以前的關係。經過JAVA內存模型的定義,提供了一種使用多種硬件、多種編譯器等優化方法仍然能正確運行代碼的約定。

Java包含多個關鍵詞volatile,final,synchronized,用來幫助程序員描述併發語義。JAVA內存模型定義了volatile和synchronized的行爲,而且確保正確同步的代碼在全部的處理器架構上都能正確執行。

爲何其餘語言沒有,如C++

其餘的大部分語言,如C或C++,並無對多線程提供直接的支持。在多種處理器架構、多種編譯器下,多線程的正確執行嚴重依賴所使用的多線程類庫,編譯器和程序運行的硬件平臺。

內存模型的歷史

原來JAVA語言規範裏面定義了一個老版本的Java內存模型,可是慢慢發現了不少缺陷,好比volatile的定義。隨後又制定了現行的JAVA內存模型,即JSR133,提供了一系列內存模型正式的語義。

什麼是指令重排序

第一篇文章已經講了指令重排序的例子。代碼實際執行時,訪問變量的指令可能會由於如下緣由與代碼順序不符:

  1. 編譯器爲優化性能重排指令
  2. 處理器在特定狀況下重排指令執行順序
  3. 數據在寄存器、處理器緩存、內存之間的移動順序
  4. 其餘的一些緣由,如JIT等

指令重排,從單線程的角度來看,規範規定了不會影響輸出結果。但若是一個變量被 多個線程同時訪問 ,重排就會影響變量的一致性。爲了可以在多線程環境下正確的訪問變量,所以須要 正確 的Synchronization。

什麼叫不正確的同步

  1. 一個線程寫入一個變量
  2. 另一個線程讀取同一個變量
  3. 對這個變量的讀寫沒有使用 同步機制 來決定順序

全部違反上述條件的都會產生競態,是不正確的同步。

同步機制是作什麼的

同步主要有如下幾種影響:

  1. 互斥排他:同時只能有一個線程得到Monitor
  2. 內存可見性:當一個線程釋放同步鎖的時候,會確保本身的寫入對其餘線程可見。多是經過數據刷入內存、其餘線程失效本地緩存等方式
  3. 禁止重排序:在同步鎖的獲取和釋放先後的代碼塊,不會重排序

新的Java內存模型在內存操做(讀字段,寫字段,lock,unlock)和線程操做(start,join)之間定義了順序,叫作一種操做 happens before 其餘操做。當一種操做happens before另一種操做時,第一個操做被確保在第二個操做以前執行,並且操做內容對第二個操做可見。具體規則以下:

  1. 單線程裏面每一個操做 happens before 代碼裏面此操做後面的操做
  2. 對一個monitor的unlock操做 happens before 在 這個monitor 上全部後續的lock操做
  3. 對一個volatile字段的寫入 happens before 對 這個字段 的全部後續讀操做
  4. 對一個線程的start操做 happens before 此啓動線程裏面的任何操做
  5. 對一個線程使用join操做,被join線程裏面的任何操做 happens before join() 調用的返回

因此,若是對一個monitor進行同步,全部釋放monitor前的操做都對後續獲取monitor的線程可見。由於全部的內存操做 happens before 所釋放, 鎖釋放 happens before 接下來的鎖獲取。

P.S. Rule1特別解釋:

rule1定義了單線程裏面全部操做都是按照代碼順序執行的,那是否是就不會產生重排序了?由於重排序後就跟代碼順序不同了。答案是,No,仍然會重排序 。具體能夠參考stackoverflow連接 重排序與happens before

It should be noted that the presence of a happens-before relationship between two actions does not necessarily imply that they have to take place in that order in an implementation. If the reordering produces results consistent with a legal execution, it is not illegal.

final是怎麼樣工做的

只要對象是被 正確的構造 的,只要這個對象構造完成,賦值給final字段的值即便沒有同步機制,對其餘全部的線程也是可見的。即便final字段是其餘對象或數組的引用,這些引用值也至少跟final字段同樣是 up to date as of the end of the object's constructor

正確的構造的含義是指在構造過程當中,該對象的引用沒有泄露。具體能夠參考連接 Safe Construction Techniques

簡單舉個沒有正確構造的例子:

public FinalFieldExample() { // bad!
  x = 3;
  y = 4;
  // bad construction - allowing this to escape
  global.obj = this;
}
複製代碼

雖說了這麼多,但若是一個線程建立了一個不可變對象(全部字段都是final),你想讓其餘線程可以正確看到這個對象,你仍是須要使用同步 。由於對這個對象的引用,若是你不使用同步機制,是沒法保證被其餘線程可見的。

volatile是幹什麼的

Volatile是用來線程間交換狀態特殊關鍵字。每次 volatile 讀都會讀到其餘任何線程上次寫入的值。每次寫入後,都會刷入內存。每次讀取前,也會失效本地緩存,直接從內存讀取。除此以外,還有特殊的限制,跟老的內存模型不一樣,新的內存模型不容許在volatile字段先後進行指令重排序。當線程 A 在寫 volatile 字段 f 前全部可見的字段都會線程 B 讀取 f 時可見。

舉例:

class VolatileExample {
  int x = 0;
  volatile boolean v = false;
  public void writer() {
    x = 42;
    v = true;
  }

  public void reader() {
    if (v == true) {
      //uses x - guaranteed to see 42.
    }
  }
}
複製代碼

因此對volatile來講就是半個synchronized,在內存可見性方面保持同樣,但不具備排他性。

double-checked locking

class Singleton{
	private static Something instance = null;
	
	public static Something getInstance() {
	  if (instance == null) {
	    synchronized (Singleton.class) {
	      if (instance == null)
	        instance = new Something();
	    }
	  }
	  return instance;
	}
}
複製代碼

上面的寫法是有問題的,你們能夠根據學到的知識進行分析一下,哪些地方存在問題?如何解決?有沒有更好的單例寫法?

引用

JSR133 FAQ




閱讀博客還不過癮?

歡迎你們掃二維碼經過添加羣助手,加入交流羣,討論和博客有關的技術問題,還能夠和博主有更多互動

博客轉載、線下活動及合做等問題請郵件至 shadowfly_zyl@hotmail.com 進行溝通

相關文章
相關標籤/搜索