1. 程式人生 > >java併發之記憶體模型

java併發之記憶體模型

java記憶體模型知識導圖

一 併發問題及含義

  併發程式設計存在原子性、可見性、有序性問題。
  1.   原子性即一系列操作要麼都執行,要麼都不執行。
  2.   可見性,一個執行緒對共享變數的修改,另一個執行緒可能不會馬上看到。由於多核CPU,每個CPU核都有快取記憶體,會快取共享變數,某個執行緒對共享變數的修改會改變快取記憶體中的值,但卻不會馬上寫入記憶體。另一個執行緒讀到的是另一個核快取的共享變數的值,出現快取不一致問題。
  3.   有序性,即程式執行的順序按照程式碼的先後順序執行。編譯器和處理器會對指令進行重排,以優化指令執行效能,重排不會改變單執行緒執行結果,但在多執行緒中可能會引起各種各樣的問題。

二 記憶體模型

  為了保證共享記憶體的正確性(可見性、有序性、原子性),記憶體模型定義了共享記憶體系統中多執行緒程式讀寫操作行為的規範。記憶體模型解決併發問題 主要採用兩種方式:限制處理器優化和使用記憶體屏障。   順序一致性記憶體模型是一種理論參考模型,提供了極強的記憶體可見性保證,具有兩大特性:
  1. 一個執行緒的所有操作按照程式的順序執行,而不能重排序。
  2. 所有執行緒只能看到單一的執行順序。每個操作都必須原子執行且立刻對其它執行緒可見。
  順序一致性記憶體模型禁止很多處理器和編譯器重排,影響執行效能,處理器記憶體模型和JMM對順序一致性記憶體模型進行放鬆,執行效能:處理器記憶體模型>JMM>順序一致性記憶體模型,易程式設計性:處理器記憶體模型<JMM<順序一致性記憶體模型。

三 java記憶體模型

  Java記憶體模型(Java Memory Model ,JMM)是一種符合記憶體模型規範的,遮蔽了各種硬體和作業系統的訪問差異的,保證了Java程式在各種平臺下對記憶體的訪問都能保證效果一致的機制及規範。   Java記憶體模型規定了所有的變數都儲存在主記憶體中,每條執行緒還有自己的工作記憶體,執行緒的工作記憶體中儲存了該執行緒中是用到的變數的主記憶體副本拷貝,執行緒對變數的所有操作都必須在工作記憶體中進行,而不能直接讀寫主記憶體。不同的執行緒之間也無法直接訪問對方工作記憶體中的變數,執行緒間變數的傳遞均需要自己的工作記憶體和主存之間進行資料同步進行。主記憶體和工作記憶體可類比成計算機記憶體模型中的主存和快取的概念。

3.1 java記憶體模型解決併發問題方法

    原子性,在java中,只有簡單的讀取、賦值(而且必須是將數字賦值給某個變數,變數之間的相互賦值不是原子操作)才是原子操作。在32位平臺下,對64位資料的賦值是需要通過兩個操作來完成,不能保證其原子性。要實現更大範圍操作的原子性,可以通過synchronized和Lock來實現。由於synchronized和Lock保證任一時刻只有一個執行緒執行該程式碼塊,從而保證了原子性。     可見性,Java提供了volatile關鍵字來保證可見性,當一個共享變數被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他執行緒需要讀取時,它會去記憶體中讀取新值。通過synchronized和Lock也能夠保證可見性,synchronized和Lock能保證同一時刻只有一個執行緒獲取鎖然後執行同步程式碼,並且在釋放鎖之前會將對變數的修改重新整理到主存當中。因此可以保證可見性。     JMM通過happens-before關係向程式設計師提供跨執行緒的記憶體可見性保證:
  1. 程式次序規則:一段程式碼在單執行緒中執行的結果是有序的。注意是執行結果,因為虛擬機器、處理器會對指令進行重排序(重排序後面會詳細介紹)。雖然重排序了,但是並不會影響程式的執行結果,所以程式最終執行的結果與順序執行的結果是一致的。故而這個規則只對單執行緒有效,在多執行緒環境下無法保證正確性。
  2. 鎖定規則:這個規則比較好理解,無論是在單執行緒環境還是多執行緒環境,一個鎖處於被鎖定狀態,那麼必須先執行unlock操作後面才能進行lock操作。
  3. volatile變數規則:這是一條比較重要的規則,它標誌著volatile保證了執行緒可見性。通俗點講就是如果一個執行緒先去寫一個volatile變數,然後一個執行緒去讀這個變數,那麼這個寫操作一定是happens-before讀操作的。
  4. 傳遞規則:提現了happens-before原則具有傳遞性,即A happens-before B , B happens-before C,那麼A happens-before C
  5. 執行緒啟動規則:假定執行緒A在執行過程中,通過執行ThreadB.start()來啟動執行緒B,那麼執行緒A對共享變數的修改在接下來執行緒B開始執行後確保對執行緒B可見。
  6. 執行緒終結規則:假定執行緒A在執行的過程中,通過制定ThreadB.join()等待執行緒B終止,那麼執行緒B在終止之前對共享變數的修改線上程A等待返回後可見。
    有序性,可以使用synchronized和volatile來保證多執行緒之間操作的有序性。實現方式有所區別:volatile關鍵字會禁止指令重排。synchronized關鍵字保證同一時刻只允許一條執行緒操作。

3.2 java併發原語

Java記憶體模型,除了定義了一套規範,還提供了一系列原語,封裝了底層實現後,供開發者直接使用。

3.2.1 volatile

記憶體語義: 當寫一個volatile變數時,JMM會把該執行緒對應的本地記憶體中的所有共享變數重新整理到主記憶體。 當讀一個volatile變數,JMM會把該執行緒對應的本地記憶體置為無效,執行緒接下來從主記憶體中讀取共享變數。 實現: 編譯器在生成位元組碼時,會在指令序列中插入記憶體屏障來禁止特定型別的處理器重排序。 在每個volatile寫操作前面插入一個StoreStore屏障。StoreStore屏障禁止上面的普通寫和volatile寫重排序,保障上面的普通寫在volatile寫之前重新整理到主記憶體。 在每個volatile寫操作後面插入一個StoreLoad屏障。避免volatile寫與後面可能有的volatile讀/寫重排序。 在每個volatile讀操作的後面插入一個LoadLoad屏障。禁止下面的普通讀操作和上面的volatile讀操作重排序 在每個volatile讀操作的後面插入一個LoadStore屏障。禁止下面的普通寫操作和上面的volatile讀操作重排序

3.2.2 synchronized

記憶體語義: 當執行緒釋放鎖時,JMM會把該執行緒對應的本地記憶體中的共享變數重新整理到主記憶體中. 當執行緒獲取鎖時,JMM會把該執行緒對應的本地記憶體置為無效。從而使得被監視器保護的臨界區程式碼必須從主記憶體中讀取共享變數. 實現: java物件頭組成:
  1. Mark Word
  2. 指向類的指標
  3. 陣列長度(只有陣列物件才有)
Mark Word用於加鎖操作,結構如下: 圖3.1 java物件頭Mark Word   synchronized用的鎖是存在Java物件頭裡,任何java物件都存在一個鎖,JVM基於進入和退出Monitor物件來實現方法同步和程式碼塊同步。程式碼塊同步是使用monitorenter和monitorexit指令實現的,monitorenter指令是在編譯後插入到同步程式碼塊的開始位置,而monitorexit是插入到方法結束處和異常處。 監視器鎖(Monitor)本質依賴作業系統的Mutex Lock(互斥鎖)來實現,如果互斥量已經上鎖,呼叫執行緒會阻塞,阻塞或喚醒一條執行緒,都需要作業系統來幫忙完成,這就需要從使用者態轉換到核心態中,因此狀態轉換需要耗費很多的處理器時間。在jdk1.6中加入對鎖的優化措施,鎖一共有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態。鎖可以升級但不能降級。   偏向鎖:   當一個執行緒訪問同步塊並獲取鎖時,會在物件頭和棧幀中的鎖記錄裡儲存鎖偏向的執行緒ID,以後該執行緒在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需簡單地測試一下物件頭的Mark Word裡是否儲存著指向當前執行緒的偏向鎖。引入偏向鎖是為了在無多執行緒競爭的情況下儘量減少不必要的輕量級鎖執行路徑,因為輕量級鎖的獲取及釋放依賴多次CAS原子指令,而偏向鎖只需要在置換ThreadID的時候依賴一次CAS原子指令(由於一旦出現多執行緒競爭的情況就必須撤銷偏向鎖,所以偏向鎖的撤銷操作的效能損耗必須小於節省下來的CAS原子指令的效能消耗)。   輕量級鎖:   輕量級鎖是為了線上程近乎交替執行同步塊時提高效能。多個執行緒競爭鎖,若當前只有一個等待執行緒,則可通過自旋稍微等待一下,可能另一個執行緒很快就會釋放鎖。 但是當自旋超過一定的次數,或者一個執行緒在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖膨脹為重量級鎖。   重量級鎖:   重量級鎖是通過物件內部的一個叫做監視器鎖(monitor)來實現的,監視器鎖本質又是依賴於底層的作業系統的Mutex Lock(互斥鎖)來實現的。而作業系統實現執行緒之間的切換需要從使用者態轉換到核心態,這個成本非常高。 其它鎖優化措施:鎖消除、鎖粗化、自旋鎖(忙迴圈,適用持有鎖的執行緒很快釋放鎖)、自適應的自旋鎖(自旋次數不固定,前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態決定)。  

3.2.3 final

  寫final域禁止把final域的寫重排序到建構函式之外。對於引用型別:在建構函式內對final域引用物件的成員域的寫入,與在建構函式外將這個被構造物件的引用賦值給引用變數,這兩個操作不能重排序。防止物件構造完成,未被初始化的final域被訪問(要達到此目的,還需確保被構造物件不能在建構函式中“逸出”) 讀final域禁止初次讀一個物件的引用和隨後初次讀這個物件包含的final域之間的重排序。確保在讀一個物件的final域前,一定會先讀包含這個final域物件的引用,如果引用不為空,引用物件的final域已經被初始化過。   實現: JMM禁止編譯器把final域的寫重排序到建構函式之外。 編譯器在final域的寫之後,建構函式return之前,插入StoreStore屏障,禁止處理器把final域的寫重排序到建構函式之外。 編譯器會在讀final域前面插入StoreStore屏障。    

 參考文獻

Java併發程式設計:volatile關鍵字解析.
java記憶體模型(JMM)總結.
不得不瞭解的物件頭.
Java synchronized原理總結.
再有人問你Java記憶體模型是什麼,就把這篇文章發給他.
JVM記憶體結構 VS Java記憶體模型 VS Java物件模型.

&nbs