併發程式設計的基礎:深入瞭解JMM

併發程式設計的基礎:深入瞭解JMM

併發和並行的理解

並行(parallel):指在同一時刻,有多條指令在多個處理器上同時執行。所以無論從微觀還是從宏觀來看,二者都是一起執行的。

併發程式設計的基礎:深入瞭解JMM

併發(concurrency):指在同一時刻只能有一條指令執行,但多個程序指令被快速的輪換執行,使得在宏觀上具有多個程序同時執行的效果,但在微觀上並不是同時執行的,只是把時間分成若干段,使多個程序快速交替的執行。

併發程式設計的基礎:深入瞭解JMM

可見性

當一個執行緒修改了共享變數的值,其他執行緒能夠看到修改的值。Java 記憶體模型是透過在變數修改後將新值同步回主記憶體,在變數讀取前從主記憶體重新整理變數值這種依賴主記憶體作為傳遞媒介的方法來實現可見性的。

如何保證可見性

透過 volatile 關鍵字保證可見性。

透過 記憶體屏障保證可見性。

透過 synchronized 關鍵字保證可見性。

透過 Lock保證可見性。

透過 final 關鍵字保證可見性

有序性

程式執行的順序按照程式碼的先後順序執行。JVM 存在指令重排,所以存在有序性問題。

如何保證有序性

透過 volatile 關鍵字保證可見性。

透過 記憶體屏障保證可見性。

透過 synchronized關鍵字保證有序性。

透過 Lock保證有序性。

原子性

一個或多個操作,要麼全部執行且在執行過程中不被任何因素打斷,要麼全部不執行。在 Java 中,對基本資料型別的變數的讀取和賦值操作是原子性操作(64位處理器)。不採取任何的原子性保障措施的自增操作並不是原子性的。

如何保證原子性

透過 synchronized 關鍵字保證原子性。

透過 Lock保證原子性。

透過 CAS保證原子性。

JMM是什麼?

Java虛擬機器規範中定義了Java記憶體模型(Java Memory Model,JMM)

用於遮蔽掉各種硬體和作業系統的記憶體訪問差異,以實現讓Java程式在各種平臺下都能達到一致的併發效果。

JMM規範了Java虛擬機器與計算機記憶體是如何協同工作的:規定了一個執行緒如何和何時可以看到由其他執行緒修改過後的共享變數的值,以及在必須時如何同步的訪問共享變數。

JMM描述的是一種抽象的概念,一組規則,透過這組規則控制程式中各個變數在共享資料區域和私有資料區域的訪問方式,JMM是圍繞原子性、有序性、可見性展開的。

併發程式設計的基礎:深入瞭解JMM

JMM與硬體記憶體架構的關係

Java記憶體模型與硬體記憶體架構之間存在差異。

硬體記憶體架構沒有區分執行緒棧和堆。

對於硬體,所有的執行緒棧和堆都分佈在主記憶體中。部分執行緒棧和堆可能有時候會出現在CPU快取中和CPU內部的暫存器中。

併發程式設計的基礎:深入瞭解JMM

記憶體互動操作方式

lock(鎖定)

:作用於主記憶體的變數,把一個變數標識為一條執行緒獨佔狀態。

unlock(解鎖)

:作用於主記憶體變數,把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他執行緒鎖定。

read(讀取)

:作用於主記憶體變數,把一個變數值從主記憶體傳輸到執行緒的工作記憶體中,以便隨後的load動作使用

load(載入)

:作用於工作記憶體的變數,它把read操作從主記憶體中得到的變數值放入工作記憶體的變數副本中。

use(使用)

:作用於工作記憶體的變數,把工作記憶體中的一個變數值傳遞給執行引擎,每當虛擬機器遇到一個需要使用變數的值的位元組碼指令時將會執行這個操作。

assign(賦值)

:作用於工作記憶體的變數,它把一個從執行引擎接收到的值賦值給工作記憶體的變數,每當虛擬機器遇到一個給變數賦值的位元組碼指令時執行這個操作。

store(儲存)

:作用於工作記憶體的變數,把工作記憶體中的一個變數的值傳送到主記憶體中,以便隨後的write的操作。

write(寫入)

:作用於主記憶體的變數,它把store操作從工作記憶體中一個變數的值傳送到主記憶體的變數中。

併發程式設計的基礎:深入瞭解JMM

記憶體互動要求的規則

如果要把一個變數從主記憶體中複製到工作記憶體,就需要按順序地執行read和load操作,如果把變數從工作記憶體中同步回主記憶體中,就要按順序地執行store和write操作。但Java記憶體模型只要求上述操作必須按順序執行,而沒有保證必須是連續執行。

不允許read和load、store和write操作之一單獨出現

不允許一個執行緒丟棄它的最近assign的操作,即變數在工作記憶體中改變了之後必須同步到主記憶體中。

不允許一個執行緒無原因地(沒有發生過任何assign操作)把資料從工作記憶體同步回主記憶體中。

一個新的變數只能在主記憶體中誕生,不允許在工作記憶體中直接使用一個未被初始化(load或assign)的變數。即就是對一個變數實施use和store操作之前,必須先執行過了assign和load操作。

一個變數在同一時刻只允許一條執行緒對其進行lock操作,但lock操作可以被同一條執行緒重複執行多次,多次執行lock後,只有執行相同次數的unlock操作,變數才會被解鎖。lock和unlock必須成對出現

如果對一個變數執行lock操作,將會清空工作記憶體中此變數的值,在執行引擎使用這個變數前需要重新執行load或assign操作初始化變數的值

如果一個變數事先沒有被lock操作鎖定,則不允許對它執行unlock操作;也不允許去unlock一個被其他執行緒鎖定的變數。

對一個變數執行unlock操作之前,必須先把此變數同步到主記憶體中(執行store和write操作)。

JMM如何保證記憶體可見性

單執行緒程式:

單執行緒程式不會出現記憶體可見性問題。編譯器、runtime和處理器會共同確保單執行緒程式的執行結果與該程式在順序一致性模型中的執行結果相同。

正確同步的多執行緒程式:

正確同步的多執行緒程式的執行將具有順序一致性(程式的執行結果與該程式在順序一致性記憶體模型中的執行結果相同)。這是JMM關注的重點,JMM透過限制編譯器和處理器的重排序來為程式設計師提供記憶體可見性保證。

未同步/未正確同步的多執行緒程式:

JMM為它們提供了最小安全性保障:執行緒執行時讀取到的值,要麼是之前某個執行緒寫入的值,要麼是預設值未同步程式在JMM中的執行時,整體上是無序的,其執行結果無法預知。JMM不保證未同步程式的執行結果與該程式在順序一致性模型中的執行結果一致。