1. 程式人生 > >Java記憶體模型(1)——JMM

Java記憶體模型(1)——JMM

在學習Java併發程式設計中,瞭解Java記憶體模型對於我們去理解Java多執行緒程式設計是非常有幫助的,本文將對JMM進行一個大體介紹,讓我們對JMM有一個大體的輪廓。

硬體的記憶體模型

 為了提高效率,充分利用計算機的能力,多工處理已經成為現代計算機的必備功能。與軟體級的併發類似,硬體級的併發也會出現併發訪問問題。例如現在的計算機一般是多核計算的,而共享記憶體區域卻只有一塊,多處理器與記憶體之間的資料共享就會出現併發問題。

 由於儲存器和儲存裝置之間的速度差了很多數量級,所以為了提高效率必須加入一層讀寫速度儘可能接近處理器運算速度的快取記憶體來作為記憶體與處理器之間的緩衝:將運算需要的資料複製到快取中,充分利用處理器計算能力,當運算結束後再從快取同步回記憶體中。雖然基於快取記憶體的儲存互動很好的解決了處理器與記憶體之間的矛盾,卻也帶來更高的複雜度:快取一致性問題。

在多處理器系統中,每個處理器都有自己的快取記憶體,它們也共享同一個主記憶體,當多個處理器的運算任務都涉及同一塊主記憶體區域時,將可能導致各自的資料不一致。

下圖是硬體的通用記憶體模型

image

Java記憶體模型

併發程式設計模型中的兩個問題
  • 執行緒之間如何通訊

     通訊是指執行緒之間以何種機制來交換資訊。在指令式程式設計中,執行緒之間的通訊機制有兩種:共享記憶體訊息傳遞

    • 在共享記憶體的併發模型中,執行緒之間共享程式的公共狀態,通過讀-寫記憶體中的公共狀態進行隱式通訊。

    • 在訊息傳遞的併發模型裡,執行緒之間沒有公共狀態,執行緒之間必須通過傳送訊息來顯式通訊。

  • 執行緒之間如何同步

    同步是指程式中用於控制不同執行緒間操作發生相對順序的機制。在共享記憶體模型中,同步是顯式進行的——程式設計師必須顯示指定某個方法或者某段程式碼需要線上程之間互斥執行。

Java併發採用的是共享記憶體記憶體模型,Java執行緒之間的通訊是隱式進行的,整個通訊過程對程式設計師完全透明。

Java記憶體模型

 Java記憶體模型(Java Memory Model, JMM)是一種由Java語言規範規定的抽象記憶體模型,旨在遮蔽掉各種硬體和作業系統在記憶體訪問上的差異,實現讓Java程式在各種平臺下都能達到一致的記憶體訪問效果。

 在Java中,所有的實力域、靜態域和陣列元素都儲存在堆記憶體中,堆記憶體中的資料是多執行緒共享的共享變數

。區域性變數、方法定義引數、異常處理器引數不會線上程之間共享,它們不會存在記憶體可見性問題,不受記憶體模型的影響。

 Java執行緒之間的通訊由Java記憶體模型來完成,JMM決定一個執行緒對共享變數的寫入何時對另一個執行緒可見。JMM定義了執行緒和主記憶體之間的抽象關係:執行緒之間的共享變數儲存在主記憶體中,每條執行緒都有一個私有的工作記憶體,工作記憶體中儲存了被該執行緒使用到的主記憶體中共享變數的副本,執行緒對變數的所有操作都必須在工作記憶體中進行,而不能直接讀寫主記憶體中的變數,不同的執行緒之間也不能直接訪問對方的工作記憶體中的變數,執行緒之間變數值的傳遞需要主記憶體來完成。下圖是執行緒,工作記憶體,主記憶體三者互動圖:

image

與前面的硬體模型類比:

  • 執行緒類比於處理器,是執行單元

  • 工作記憶體類比於快取記憶體

  • 主記憶體類比於硬體模型中的主記憶體(JMM中的主記憶體只是虛擬機器的一部分)

主記憶體與工作記憶體之間的互動操作

 Java記憶體模型定義了8種用於完成主記憶體和工作記憶體之間互動協議的操作,即一個變數如何從主記憶體拷貝到工作記憶體、如何從工作記憶體同步回主記憶體的實現細節。虛擬機器實現時必須保證8種操作都是原子的、不可再分的:

  • lock(鎖定): 作用於主記憶體的變數,把一個變數標識為執行緒獨佔的狀態,即某一時刻只能有一條執行緒擁有此變數,其他執行緒只能等待

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

  • read(讀取): 作用於主記憶體的變數,把一個變數的值從主記憶體傳送到執行緒的工作記憶體中,以便隨後的load操作使用

  • load(載入): 作用於工作記憶體的變數,把read操作從主記憶體得到的變數的值放入工作記憶體的變數副本中

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

  • assign(賦值): 作用工作記憶體的變數,把一個從執行引擎收到的值賦給工作記憶體的變數,沒當虛擬機器遇到一個給變數賦值的位元組碼指令時執行這個操作

  • store(儲存): 作用於工作記憶體的變數,把工作記憶體中一個變數的值傳送到主記憶體中,以便隨後的write操作使用

  • write(寫入): 作用於主記憶體的變數,把store操作從工作記憶體中得到的變數的值放入到主記憶體的變數中

相應的這8個操作必須遵守以下規則:

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

  • 不允許執行緒丟棄最近德assign操作,即變數在工作記憶體中改變了之後必須把變化同步到主記憶體

  • 沒有發生assign操作的變數不能同步到主記憶體

  • 對一個變數實時use、store操作之前,必須先執行過assign和load操作,即一個新的變數只能由主記憶體建立,不允許在工作記憶體中直接使用一個未被初始化的變數(初始化: load和assign)

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

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

  • 如果一個變數事先沒有被lock操作鎖定,那就不允許對它進行unlock操作,也不允許去unlock一個被其他執行緒鎖定的變數(lock和unlock對應於同一條執行緒)

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

併發的三個特性

 Java記憶體模型是圍繞著在併發過程中如何處理原子性,可見性和有序性3個特徵建立的,下面簡單介紹一下這三個特性。

  • 原子性: 一個操作是不可分割的整體,要麼都發生,要麼都不發生。Java記憶體模型直接保證的原子性變數操作包括read、load、assign、use、store和write。我們可以認為基本資料型別的訪問讀寫是具備原子性的(long和double的非原子協定例外)。Java記憶體模型還提供了lock和unlock操作來滿足更大範圍的原子性保證,lock和unlock操作沒有直接開放給程式設計師使用,但是提供了更高層次的位元組碼指令monitorentermonitorexit隱式的表達這兩個操作,反映到Java程式碼中就是同步塊——synchronized關鍵字因此synchronized塊之間的操作具有原子性。

  • 可見性: 指當一個執行緒修改了共享變數的值,其他執行緒能夠立即得知這個修改。Java記憶體模型通過在變數修改後將新值同步回主記憶體,在變數讀取前從主記憶體重新整理變數值這種依賴主記憶體作為傳播媒介的方式來實現可見性的,無論是普通變數還是volatile變數都如此。普通變數和volatile的區別在於:volatile保證新值能夠立即同步到主記憶體,每次使用前立即從主記憶體重新整理。Java語言中,volatile、synchronized、final都能保證可見性

  • 有序性: 在本執行緒內觀察,所有操作都是有序的(執行緒內序列執行);在一個執行緒中觀察另一個執行緒,所有操作都是無序的(指令重排序記憶體與主記憶體之間的同步延遲)。Java語言中,volatile、synchronized能保證執行緒之間操作的有序性

 總結:synchronized可以保證原子性,可見性和有序性;volatile可以保證可見性和有序性,無法保證原子性(但是可以保證long或double型別的變數的原子性);final可以保證可見性。總之,合理使用併發機制是提高併發能力的關鍵因素。

long和double的非原子協定

 前面說過,基本資料型別的訪問讀寫是具備原子性的,但是long和double除外。JMM要求lock、unlock、read、load、assign、use、store、write這8個操作都具有原子性,對於64位的資料型別(long和double),JMM特別定義了一條相對寬鬆的規定:允許虛擬機器將沒有被volatile修飾的64位資料的讀寫操作劃分為兩次32位的操作來進行,即不保證load、store、read和write的原子性,這就是long和double的非原子性協定

 如果多個執行緒共享一個並未宣告為volatile的long或double型別的變數,並且同時對他們進行讀取和修改操作,那麼有些執行緒可能只會讀到一個半值(高32位或低32位)。但是目前各種平臺下的商用虛擬機器幾乎都將64位的資料讀寫操作作為原子操作來對待,所以我們不必將其專門宣告為volatile。

重排序

 重排序是編譯器和處理器為了提高程式執行效能而做的一些指令序列優化,主要分為三種類型:

  • 編譯器優化重排序: 編譯器在不改變單執行緒語義的前提下,可以重新安排語句的執行序列。

  • 指令級並行重排序: 現在的處理器採用了指令集並行來講多條指令重疊執行。如果不存在資料依賴性,處理器可以改變語句對應機器指令的執行順序。

  • 記憶體系統的重排序: 處理器使用快取和讀/寫緩衝區,使得載入和儲存操作看上去是在亂序執行。

從原始碼到指令序列會經歷如下3中重排序:

image

 上圖中1屬於編譯器重排序,2和3屬於處理器重排序,重排序會導致出現記憶體可見性問題。對於編譯器,JMM的編譯器重排序規則會禁止部分重排序。對於處理器(JVM),JMM規則要求在Java編譯器在生成指令序列時,插入特定的記憶體屏障指令來消除特定型別的處理器重排序(volatile的語義之一就是禁止指令重排序,所以可以解決可見性問題)。

執行緒的浪漫生活

image

參考