1. 程式人生 > 程式設計 >java併發程式設計——記憶體模型

java併發程式設計——記憶體模型

1. 併發程式設計基礎概念

併發——在作業系統中,是指一個時間段中有幾個程式都處於已啟動執行到執行完畢之間,且這幾個程式都是在同一個處理機上執行,但任一個時刻點上只有一個程式在處理機上執行——源自百度百科

在併發程式設計中,我們需要處理兩個關鍵問題:執行緒之間如何通訊執行緒之間如何同步,後續篇章將圍繞這兩個問題進行介紹。

  • 執行緒通訊:是指執行緒之間以何種機制來交換資訊,在指令式程式設計中,執行緒之間的通訊機制有兩種:共享記憶體和訊息傳遞
  • 執行緒同步:是指程式用於控制不同執行緒之間操作發生相對順序的機制。在Java中,可以通過volatile,synchronized, 鎖等方式實現同步。

本文主要介紹java的通訊機制,剛介紹常見通訊機制主要包括以下兩種方式:

  1. 共享記憶體:執行緒之間共享程式的公共狀態,執行緒之間通過寫-讀記憶體中的公共狀態來隱式進行通訊。
  2. 訊息傳遞:執行緒之間沒有公共狀態,執行緒之間必須通過明確的傳送訊息來顯式進行通訊。

Java的併發採用的是共享記憶體模型,Java執行緒之間的通訊總是隱式進行,整個通訊過程對程式設計師完全透明。在java中,所有例項域、靜態域和陣列元素儲存在堆記憶體中,堆記憶體線上程之間共享。

Java執行緒之間的通訊由Java記憶體模型(本文簡稱為JMM)控制,JMM決定一個執行緒對共享變數的寫入何時對另一個執行緒可見。

2. JMM記憶體模型

JMM(Java Memory Model)是JVM規範中定義的一種Java記憶體模型,它的目的是遮蔽掉各種硬體和作業系統的記憶體訪問差異,以實現讓Java程式在各種平臺上到能達到一致的記憶體訪問效果。 Java記憶體模型的主要定義程式中各個變數的訪問規則,即在虛擬機器器中將變數儲存到記憶體和從記憶體中取出變數這樣底層細節。首先簡單說明幾個常用名稱定義:

  • 變數:這裡指包括了例項欄位、靜態欄位和構成陣列物件的元素,但是不包括區域性變數與方法引數,後者是執行緒私有的,不會被共享。
  • 主記憶體:在java中,例項域、靜態域和陣列元素是執行緒之間共享的資料,它們儲存在主記憶體中。
  • 工作記憶體:每條執行緒都有自己的工作記憶體,執行緒的工作記憶體中儲存了該執行緒使用到的變數到主記憶體副本拷貝,執行緒對變數的所有操作(讀取、賦值)都必須在工作記憶體中進行,而不能直接讀寫主記憶體中的變數。

執行緒、主記憶體和工作記憶體之間互動關係

執行緒、主記憶體和工作記憶體的互動關係如上圖所示,和CPU-快取-記憶體很類似。 不同執行緒之間無法直接訪問對方工作記憶體中的變數,執行緒間變數值的傳遞均需要在主記憶體來完成。 最後注意,為了獲得較好的執行效能,Java記憶體模型並沒有限制執行引擎使用處理器的暫存器或者快取記憶體來提升指令執行速度,也沒有限制編譯器對指令進行重排序。也就是說,在java記憶體模型中,也會存在快取一致性問題和指令重排序的問題

3. 記憶體間互動操作

關於主記憶體與工作記憶體之間的具體互動協議,即一個變數如何從主記憶體拷貝到工作記憶體、如何從工作記憶體同步到主記憶體之間的實現細節,Java記憶體模型定義了以下八種操作來完成:

  • lock(鎖定):作用於主記憶體的變數,把一個變數標識為一條執行緒獨佔狀態。
  • unlock(解鎖):作用於主記憶體變數,把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他執行緒鎖定。
  • read(讀取):作用於主記憶體變數,把一個變數值從主記憶體傳輸到執行緒的工作記憶體中,以便隨後的load動作使用
  • load(載入):作用於工作記憶體的變數,它把read操作從主記憶體中得到的變數值放入工作記憶體的變數副本中。
  • use(使用):作用於工作記憶體的變數,把工作記憶體中的一個變數值傳遞給執行引擎,每當虛擬機器器遇到一個需要使用變數的值的位元組碼指令時將會執行這個操作。
  • assign(賦值):作用於工作記憶體的變數,它把一個從執行引擎接收到的值賦值給工作記憶體的變數,每當虛擬機器器遇到一個給變數賦值的位元組碼指令時執行這個操作。
  • store(儲存):作用於工作記憶體的變數,把工作記憶體中的一個變數的值傳送到主記憶體中,以便隨後的write的操作。
  • write(寫入):作用於主記憶體的變數,它把store操作從工作記憶體中一個變數的值傳送到主記憶體的變數中。

所以變數讀寫包含以下幾個步驟:

  1. 變數從主記憶體複製到工作記憶體——順序執行read和load操作
  2. 變數從工作記憶體同步到主記憶體——順序執行store和write操作

注意,Java記憶體模型只要求上述操作必須按順序執行,而沒有保證必須是連續執行。也就是read和load之間,store和write之間是可以插入其他指令的。 除了定義以上8中原子操作,Java記憶體模型還規定了上述8種基本操作在執行時必須滿足一定的操作規則,例如如不允許read和load單獨出現(即不允許一個變數從主記憶體中讀取但工作記憶體不接受),不允許store和write單獨出現(即不允許從工作記憶體中發起了回寫單主記憶體不接受),這裡不一一列舉,詳細網上搜索即可。 Java記憶體模型還定義了volatile型變數的特殊規則(下一節介紹),以上三種規定共同確定了Java中哪些記憶體訪問操作是安全的即:

8種原子操作+操作規則+volatile規定=Java中哪些記憶體訪問操作是安全的

4. volatile型變數規定

當一個變數被定義為volatile後,將具備兩種特性:

  • 特性一:保證此變數對所有執行緒的可見性
  • 特性二:禁止指令重排序優化

4.1 volatile可見性

可見性是指當多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看得到修改的值。 普通的共享變數不能保證可見性,因為普通共享變數被修改之後,什麼時候被寫入主存是不確定的,當其他執行緒去讀取時,此時記憶體中可能還是原來的舊值,因此無法保證可見性。 但是,需要注意的是volatile變數只保證可見性,但是java裡面的運算並非全部都是原子操作例如++操作,這樣同樣導致volatile修飾變數java運算不安全。 一般不符合以下兩條規則的運算場景中,我們需要通過加鎖(synchronized或併發包中的鎖)保證變數原子性:

  • 運算結果並不依賴變數的當前值,或者能夠確保只有單一的執行緒修改變數的值(比如++操作不符合依賴當前值)
  • 變數不需要與其他狀態變數共同參與不變約束

常見的volatile修飾變數的場景是用來作為開關控制併發:

volatile開關

4.2 禁止指令重排序

重排序:是指“編譯器和處理器”為了提高效能,而在程式執行時會對程式進行的重排序。大致可以分為以下三類:

  • 編譯器優化指令重排,不改變單執行緒語義的情況下,重新安排指令執行的順序。
  • 指令級並行重排序,該優化主要是為了讓程式發揮現代處理器的指令級並行執行能力,前提是這些語句不存在資料依賴。
  • 記憶體系統重排序,主要發生在處理器讀寫緩衝區,讀寫過程看起來是無序的,但最終結果是有序的 從Java原始碼到最終實際執行的指令序列,會經過下面三種重排序:

實際執行指令序列
以上重排序可能會導致多執行緒中出現記憶體可見性問題,針對編譯器重排序JMM的編譯器重排序規則會禁止特定型別的編譯器重排序。 而對於後兩種重排序,JMM的處理器重排序規則會要求java編譯器在生成指令序列時,插入特定型別的記憶體屏障(memory barriers,intel稱之為memory fence)指令,通過記憶體屏障指令來禁止特定型別的處理器重排序(不是所有的處理器重排序都要禁止)。

下面我們看下jvm如何實現volatile禁止指令重排序的:

  1. volatile變數寫操作,jvm會向處理器傳送一條Lock字首命令,將變數所在的快取行系會到系統記憶體。其他處理器通過嗅探匯流排上傳播的資料檢測自己的資料是否過期,如果發現過期會置為無效,再次使用時會從系統記憶體獲取
  2. Lock字首命令禁止該指令與之前和之後的讀和寫指令重排序。

最後,關於volatile禁止重排序幾點使用說明:

  • 不會對volatile讀與volatile讀後面的任意記憶體操作重排序
  • 不會對volatile寫與volatile寫之前的任意記憶體操作重排序
  • CAS同時具有volatile讀和寫記憶體的語義,java的CAS使用現代處理器提供的高效級別的原子指令,這些原子指令以原子方式對記憶體執行讀-改-寫操作,這是多處理器中實現同步的關鍵。

5. JMM記憶體模型總結

總的來說JMM記憶體模型是圍繞著在併發過程中如何處理原子性、可見性和有序性三個特徵來建立的。下面就三個特徵分別說明:

5.1 原子性

原子性:即一個操作或者多個操作 要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。 java記憶體模型的read、load、assign、use、store和write六個操作直接保證原子性,我們可以任務基本資料型別訪問讀寫是具有原子性(特殊說明long double64位操作根據jvm實現有關)。 如果場景中需要大範圍的原子性保證,java記憶體模型提供了lock和unlock操作來滿足,對應到java程式碼關鍵字即是——synchronized。

5.2 可見性

可見性是指當多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看得到修改的值。 除了上面介紹的volatile外,java還提供了兩個關鍵字實現可見性,synchronized和final。

  • final的可見性:是指被final修飾的欄位在構造器中一旦完成,那麼在其他執行緒就可以看見final欄位值
  • synchronized可見性:是指對一個變數執行unlock操作之前,必須先把次變數同步會主記憶體這條操作規則限制

5.3 有序性

有序性:即程式執行的順序按照程式碼的先後順序執行。 java中天然有序性可以總結為一句話:如果在本執行緒內觀察,所有的操作都是有序的;如果在一個執行緒中觀察另外一個執行緒,所有操作都是無序的。前半句是指“執行緒內表現為序列語義”,後半句表示“指令重排”和“工作記憶體與主記憶體同步延遲”現象。 java提供了volatile和synchronized兩個關鍵字來保證執行緒之間操作的有序性,這裡synchronized則是有“同一時刻只允許一條執行緒對其進行lock操作”這條操作規定獲取的,這個規則決定了同一個鎖的兩個同步塊只能序列進入。

最後,可以發現synchronized關鍵字可以同時解決上述三個問題,當然這個需要付出代價就是效能問題。

參考檔案

《深入理解java虛擬機器器》——周志明 www.cnblogs.com/dolphin0520…