1. 程式人生 > >Java 並發學習之 JMM

Java 並發學習之 JMM

for 多個 有一個 詳細介紹 過程 不可見 指令重排 都是 重排序

Java 並發學習之 JMM

順序一致性模型與 JMM

順序一致性模型是一種理想的內存模型,在這個模型下,指令是嚴格按照代碼的編寫順序執行,同時所有線程只能看到同一個內存區且對內存區的操作都是互斥的,內存對所有線程都是可見的。

JMM 中,由於每個線程有自己的工作內存,很多情況下,只是對工作內存中的變量副本進行修改而未真正同步到主內存中,因此每個線程對內存的更改對其他線程都是不可見的,同時出於對性能的優化,指令的順序經過編譯器和處理器的重排,其執行的順序跟代碼的編寫順序很有可能不一樣。所以導致了 JMM 不可能像順序一致性模型那樣具有極強的內存可見性保證,在 JMM 中如果要一個操作執行的結果對另一個操作可見,那麽這兩個操作必須滿足 happens-before

原則。

什麽是指令重排?

指令重排是編譯器和處理器用於優化程序性能的手段,舉個例子,指令 B 的執行依賴指令 A 的結果,此時有一些對內存的訪問操作就可以插入指令 B 和指令 A 之間,以提高 IO 的效率。指令重排有一個基本原則,就是在單線程中遵守數據的依賴關系(寫寫、寫讀、讀寫指令間都是有依賴關系,不允許重排),從而保證單線程中執行結果的一致性。

本文將從happens-before 原則出發,分析 JMM 中用於提供內存可見性的設計。

happens-before

volatile

volatile 變量具有可見性原子性。可見性是指對一個 volatile 變量的讀總是能夠看到對這個 volatile 變量的最後寫入;原子性是指對 volatile 變量的讀操作和寫操作具有原子性,復合操作(volatile++)不具有。

為了保證 volatile 變量的兩個特性,JMM 在對 volatile 變量的讀操作和寫操作進行以下的設計:

  1. 當寫一個 volatile 變量時,JMM 會將線程工作內存的變量刷新到主內存。
  2. 當讀一個 volatile 變量時,JMM 會將工作內存的變量置為無效,直接從主內存中獲取。

由於指令的重排序導致多個線程下,變量的讀寫依賴無法被滿足,JMM 對 volatile 變量的指令重排做了限制:

  1. volatile 寫之前的操作不能被重排到 volatile 寫之後。
  2. volatile 讀之後的操作不能被重排到 volatile 讀之前。
  3. 當第一個操作是 volatile 寫,第二個操作是 volatile 讀時,不能重排。

這些限制都是通過 JMM 內存屏障來實現的。內存屏障的細節本文不做詳細介紹。

針對鎖的獲取到釋放過程遵循以下三個 happens-before 關系:

  1. 程序次序規則:先獲取鎖,再執行臨界區代碼
  2. 監視器規則:先釋放鎖,再獲取鎖
  3. happens-before 傳遞性:先獲取鎖的臨界區代碼先執行

為了保證內存的可見性,JMM 在鎖的釋放和獲取操作進行以下的設計:

  1. 當線程釋放鎖時,會把該線程對應的工作內存的變量刷新到主內存。
  2. 當線程獲取鎖時,會把線程對應的工作內存的變量置為無效,從而使得臨界區代碼必須從主內存中讀取變量。

JMM 鎖的釋放和獲取操作的可見性實際上是通過對 volatile 變量的操作來實現的。(參考 AQS 的實現)

final 域

JMM 對 final 域的重排序也做了限制:

  1. 在構造函數內對 final 域的寫入,與隨後將被構造對象的引用賦值給一個引用對象,這兩個操作不能重排。
  2. 初次讀包含 final 域對象的引用,與隨後初次讀這個 final 域,這兩個操作不能重排。

這些限制也是通過 JMM 內存屏障實現的。

線程

在線程 A 中執行操作 ThreadB.start(),A 線程中的 ThreadB.start() 操作 happens-before 線程 B 中的任意操作。在線程 A 中執行操作 ThreadB.join(),B 線程中的任意操作 happens-before 線程 A 從 Thread.join() 操作返回。

Java 並發學習之 JMM