Java內存模型

2.7 Java內存模型

2.7.1 并發編程模型

在并發編程中,需要處理兩個關鍵問題:線程之間如何通信及線程之間如何同步。

通信是指線程之間以何種機制來交換信息,在命令式編程中,線程之間的通信機制有兩種:共享內存和消息傳遞。

  • 在共享內存的并發模型里,線程之間共享程序的公共狀態,線程之間通過寫-讀內存中的公共狀態來隱式進行通信。
  • 在消息傳遞的并發模型里,線程之間沒有公共狀態,線程之間必須通過明確的發送消息來顯式進行通信。

同步是指程序用于控制不同線程之間操作發生相對順序的機制。

  • 在共享內存的并發模型里,同步是顯式進行的,程序員必須顯式指定某個方法或某段代碼需要在線程之間互斥執行。
  • 在消息傳遞的并發模型里,由于消息的發送必須在消息的接收之前,因此同步是隱式進行的。

Java采用的共享內存模型,線程間通信是隱式的,線程間同步是顯式的。

2.7.2 Java內存模型

在Java中,所有實例域、靜態域和數組元素存儲在堆內存中,堆內存在線程之間共享(本文使用“共享變量”這個術語代指實例域,靜態域和數組元素)。局部變量、方法定義參數和異常處理器參數不會在線程之間共享,它們不會有內存可見性問題,也不受內存模型的影響。

Java線程之間的通信由Java內存模型(本文簡稱為JMM)控制,JMM決定一個線程對共享變量的寫入何時對另一個線程可見。從抽象的角度來看,JMM定義了線程和主內存之間的抽象關系:線程之間的共享變量存儲在主內存中,每個線程都有一個私有的本地內存,本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是JMM的一個抽象概念,并不真實存在,它涵蓋了緩存,寫緩沖區,寄存器以及其他的硬件和編譯器優化。Java內存模型的抽象示意圖如下:

從上圖來看,線程A與線程B之間如要通信的話,必須要經歷下面2個步驟:首先線程A把本地內存A中更新過的共享變量刷新到主內存中去;然后線程B到主內存中去讀取線程A之前已更新過的共享變量。

2.7.3 重排序和happens-before

重排序

為了提高性能,編譯器和處理器常常會對程序做重排序。重排序分三種類型:

  1. 編譯器優化的重排序:編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執行順序。
  2. 指令級并行的重排序:現代處理器采用指令級并行技術來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。
  3. 內存系統的重排序:由于處理器使用緩存和讀/寫緩沖區,這使得加載和存儲操作看上去可能是在亂序執行。

前者屬于編譯器重排序,后兩者屬于處理器重排序。重排序可能會導致多線程程序出現內存可見性問題。JMM屬于語言級的內存模型,它確保在不同的編譯器和不同的處理器平臺之上,通過禁止特定類型的編譯器重排序和處理器重排序,為程序員提供一致的內存可見性保證。

happens-before

在JMM中,如果一個操作執行的結果需要對另一個操作可見,那么這兩個操作之間就存在happens-before關系。這里提到的兩個操作既可以是在一個線程之內,也可以是在不同線程之間。與編程密切相關的happens-before規則如下:

  1. 程序順序規則:一個線程中的每個操作,happens-before于該線程中的任意后續操作
  2. 監視器鎖規則:對一個監視器的解鎖,happens-before于隨后對這個監視器的加鎖
  3. volatile變量規則:對一個volatile域的寫,happens-before于任意后續對這個volatile域的讀
  4. 傳遞性:如果A happens-before B,且B happens-before C,那么A happens-before C

注意,兩個操作之間具有happens-before關系,并不意味著前一個操作必須要在后一個操作之前執行。happens-before僅僅要求前一個操作(執行的結果)對后一個操作可見,且前一個操作按順序排在第二個操作之前。happens-before的定義很微妙。

happens-before與JMM的關系如下圖所示:

如上圖所示,一個happens-before規則對應于一個或多個編譯器和處理器重排序規則。對于Java程序員來說,happens-before規則簡單易懂,它避免Java程序員為了理解JMM提供的內存可見性保證而去學習復雜的重排序規則以及這些規則的具體實現。

2.7.4 volatile、synchronized和final

volatile

volatile變量有以下兩個特性:

  1. 可見性:對一個volatile變量的讀,總是可以看到(任意線程)對該volatile變量最后的寫入。
  2. 原子性:對任意單個volatile變量的讀/寫具有原子性,但是類似于volatile++這樣的復合操作不具有原子性。

對程序員來說,volatile對線程的內存可見性的影響比volatile自身的特性更為重要。接下來我們將會簡介volatile讀/寫的happens-before關系:

  1. volatile寫的內存語義:當寫一個volatile變量時,JMM會把該變量值刷新到主內存。
  2. volatile讀的內存語義:當讀一個volatile變量時,JMM會把該線程對應的本地內存置為無效,從主內存中讀取共享變量。

synchronized

鎖的內存語義和volatile的基本相同:

  1. 釋放鎖時,會將該鎖保護的臨界區代碼中的共享變量刷新到主內存中。
  2. 獲取鎖時,會將該線程對應的本地內存置為無效,該鎖保護的臨界區代碼必須從主內存中讀取共享變量。

需要注意的是當我們使用synchronized時,需要注意鎖對象:當synchronized作用在方法上時,鎖住的便是對象實例(this);當作用在靜態方法時鎖住的便是對象對應的Class實例,因為Class數據存在于永久帶,因此靜態方法鎖相當于該類的一個全局鎖。

final

與前面介紹的鎖和volatile相比較,對final域的讀和寫更像是普通的變量訪問。對于final域,編譯器和處理器要遵守兩個重排序規則:

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

使用final時,不要在構造函數中逸出被構造對象的引用。

2.7.5 順序一致性

順序一致性內存模型

順序一致性內存模型是一個理想化模型,它為程序員提供了極強的內存可見性保證。順序一致性內存模型有兩大特性:

  1. 一個線程中的所有操作必須按照程序的順序來執行。
  2. (不管程序是否同步)所有線程都只能看到一個單一的操作執行順序。在順序一致性內存模型中,每個操作都必須原子執行且立刻對所有線程可見。

JMM

上文提到順序一致性內存模型是一個理想化模型。JMM并不遵循該模型,未同步程序在JMM中的執行順序是無序的,而且所有線程看到的操作執行順序也可能不一致。比如,在線程A把寫過的數據緩存在本地內存中,在還沒有刷新到主內存之前,這個寫操作僅對當前線程可見;從其他線程的角度來觀察,會認為這個寫操作根本還沒有被執行過。只有線程A把數據刷新到主內存之后,這個寫操作才能對其他線程可見。

之所以如此,是平衡的結果,內存模型越強,編譯器和處理器能做的優化就越少,性能就越差。而Java內存可見性保證:

  1. 單線程,單線程不存在內存可見性問題.
  2. 正確同步的多線程,正確同步的多線程程序的執行具有順序一致性.
  3. 未正確同步的多線程,最小安全性保障:線程執行時讀取到的值,要么是之前某個線程寫入的值,要么是默認值(0、null、false)。

//TODO
6個原子操作:read,write,use,assign,lock,unlock

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容