Java 記憶體模型 JMM 淺析

来源:https://www.cnblogs.com/AIPAOJIAO/archive/2018/08/14/9478140.html
-Advertisement-
Play Games

JMM簡介 Java Memory Model簡稱JMM, 是一系列的Java虛擬機平臺對開發者提供的多線程環境下的記憶體可見性、是否可以重排序等問題的無關具體平臺的統一的保證。(可能在術語上與Java運行時記憶體分佈有歧義,後者指堆、方法區、線程棧等記憶體區域)。併發編程有多種風格,除了CSP(通信順序 ...


JMM簡介

Java Memory Model簡稱JMM, 是一系列的Java虛擬機平臺對開發者提供的多線程環境下的記憶體可見性、是否可以重排序等問題的無關具體平臺的統一的保證。(可能在術語上與Java運行時記憶體分佈有歧義,後者指堆、方法區、線程棧等記憶體區域)。
併發編程有多種風格,除了CSP(通信順序進程)、Actor等模型外,大家最熟悉的應該是基於線程和鎖的共用記憶體模型了。在多線程編程中,需要註意三類併發問題:

  1. 原子性
  2. 可見性
  3. 重排序

原子性涉及到,一個線程執行一個複合操作的時候,其他線程是否能夠看到中間的狀態、或進行干擾。典型的就是i++的問題了,兩個線程同時對共用的堆記憶體執行++操作,而++操作在JVM、運行時、CPU中的實現都可能是一個複合操作, 例如在JVM指令的角度來看是將i的值從堆記憶體讀到操作數棧、加上一、再寫回到堆記憶體的i,這幾個操作的期間,如果沒有正確的同步,其他線程也可以同時執行,可能導致數據丟失等問題。常見的原子性問題又叫競太條件,是基於一個可能失效的結果進行判斷,如讀取-修改-寫入。 可見性和重排序問題都源於系統的優化。

由於CPU的執行速度和記憶體的存取速度嚴重不匹配,為了優化性能,基於時間局部性、空間局部性等局部性原理,CPU在和記憶體間增加了多層高速緩存,當需要取數據時,CPU會先到高速緩存中查找對應的緩存是否存在,存在則直接返回,如果不存在則到記憶體中取出並保存在高速緩存中。現在多核處理器越基本已經成為標配,這時每個處理器都有自己的緩存,這就涉及到了緩存一致性的問題,CPU有不同強弱的一致性模型,最強的一致性安全性最高,也符合我們的順序思考的模式,但是在性能上因為需要不同CPU之間的協調通信就會有很多開銷。

典型的CPU緩存結構示意圖如下

CPU的指令周期通常為取指令、解析指令讀取數據、執行指令、數據寫回寄存器或記憶體。串列執行指令時其中的讀取存儲數據部分占用時間較長,所以CPU普遍採取指令流水線的方式同時執行多個指令, 提高整體吞吐率,就像工廠流水線一樣。

讀取數據和寫回數據到記憶體相比執行指令的速度不在一個數量級上,所以CPU使用寄存器、高速緩存作為緩存和緩衝,在從記憶體中讀取數據時,會讀取一個緩存行(cache line)的數據(類似磁碟讀取讀取一個block)。數據寫回的模塊在舊數據沒有在緩存中的情況下會將存儲請求放入一個store buffer中繼續執行指令周期的下一個階段,如果存在於緩存中則會更新緩存,緩存中的數據會根據一定策略flush到記憶體。

  1.   public class MemoryModel {
  2.       private int count;
  3.       private boolean stop;
  4.       public void initCountAndStop() {
  5.           count = 1;
  6.           stop = false;
  7.       }
  8.       public void doLoop() {
  9.           while(!stop) {
  10.               count++;
  11.           }
  12.       }
  13.       public void printResult() {
  14.           System.out.println(count);
  15.           System.out.println(stop);
  16.       }
  17.   }

上面這段代碼執行時我們可能認為count = 1會在stop = false前執行完成,這在上面的CPU執行圖中顯示的理想狀態下是正確的,但是要考慮上寄存器、緩存緩衝的時候就不正確了, 例如stop本身在緩存中但是count不在,則可能stop更新後再count的write buffer寫回之前刷新到了記憶體。

另外CPU、編譯器(對於Java一般指JIT)都可能會修改指令執行順序,例如上述代碼中count = 1和stop = false兩者並沒有依賴關係,所以CPU、編譯器都有可能修改這兩者的順序,而在單線程執行的程式看來結果是一樣的,這也是CPU、編譯器要保證的as-if-serial(不管如何修改執行順序,單線程的執行結果不變)。由於很大部分程式執行都是單線程的,所以這樣的優化是可以接受並且帶來了較大的性能提升。但是在多線程的情況下,如果沒有進行必要的同步操作則可能會出現令人意想不到的結果。例如線上程T1執行完initCountAndStop方法後,線程T2執行printResult,得到的可能是0, false, 可能是1, false, 也可能是0, true。如果線程T1先執行doLoop(),線程T2一秒後執行initCountAndStop, 則T1可能會跳出迴圈、也可能由於編譯器的優化永遠無法看到stop的修改。

由於上述這些多線程情況下的各種問題,多線程中的程式順序已經不是底層機制中的執行順序和結果,編程語言需要給開發者一種保證,這個保證簡單來說就是一個線程的修改何時對其他線程可見,因此Java語言提出了JavaMemoryModel即Java記憶體模型,對於Java語言、JVM、編譯器等實現者需要按照這個模型的約定來進行實現。Java提供了Volatile、synchronized、final等機制來幫助開發者保證多線程程式在所有處理器平臺上的正確性。

在JDK1.5之前,Java的記憶體模型有著嚴重的問題,例如在舊的記憶體模型中,一個線程可能在構造器執行完成後看到一個final欄位的預設值、volatile欄位的寫入可能會和非volatile欄位的讀寫重排序。

所以在JDK1.5中,通過JSR133提出了新的記憶體模型,修複之前出現的問題。

重排序規則

 

volatile和監視器鎖

是否可以重排序第二個操作第二個操作第二個操作
第一個操作 普通讀/普通寫 volatile讀/monitor enter volatile寫/monitor exit
普通讀/普通寫     No
voaltile讀/monitor enter No No No
volatile寫/monitor exit   No No

其中普通讀指getfield, getstatic, 非volatile數組的arrayload, 普通寫指putfield, putstatic, 非volatile數組的arraystore。

volatile讀寫分別是volatile欄位的getfield, getstatic和putfield, putstatic。

monitorenter是進入同步塊或同步方法,monitorexist指退出同步塊或同步方法。

上述表格中的No指先後兩個操作不允許重排序,如(普通寫, volatile寫)指非volatile欄位的寫入不能和之後任意的volatile欄位的寫入重排序。當沒有No時,說明重排序是允許的,但是JVM需要保證最小安全性-讀取的值要麼是預設值,要麼是其他線程寫入的(64位的double和long讀寫操作是個特例,當沒有volatile修飾時,並不能保證讀寫是原子的,底層可能將其拆分為兩個單獨的操作)。

final欄位

final欄位有兩個額外的特殊規則

  1. final欄位的寫入(在構造器中進行)以及final欄位對象本身的引用的寫入都不能和後續的(構造器外的)持有該final欄位的對象的寫入重排序。例如, 下麵的語句是不能重排序的
    x.finalField = v; ...; sharedRef = x;
    
  2. final欄位的第一次載入不能和持有這個final欄位的對象的寫入重排序,例如下麵的語句是不允許重排序的
    x = sharedRef; ...; i = x.finalField
    

記憶體屏障

處理器都支持一定的記憶體屏障(memory barrier)或柵欄(fence)來控制重排序和數據在不同的處理器間的可見性。例如,CPU將數據寫回時,會將store請求放入write buffer中等待flush到記憶體,可以通過插入barrier的方式防止這個store請求與其他的請求重排序、保證數據的可見性。可以用一個生活中的例子類比屏障,例如坐地鐵的斜坡式電梯時,大家按順序進入電梯,但是會有一些人從左側繞過去,這樣出電梯時順序就不相同了,如果有一個人攜帶了一個大的行李堵住了(屏障),則後面的人就不能繞過去了:)。另外這裡的barrier和GC中用到的write barrier是不同的概念。

記憶體屏障的分類

幾乎所有的處理器都支持一定粗粒度的barrier指令,通常叫做Fence(柵欄、圍牆),能夠保證在fence之前發起的load和store指令都能嚴格的和fence之後的load和store保持有序。通常按照用途會分為下麵四種barrier

LoadLoad Barriers

Load1; LoadLoad; Load2;

保證Load1的數據在Load2及之後的load前載入

StoreStore Barriers

Store1; StoreStore; Store2

保證Store1的數據先於Store2及之後的數據 在其他處理器可見

LoadStore Barriers

Load1; LoadStore; Store2

保證Load1的數據的載入在Store2和之後的數據flush前

StoreLoad Barriers

Store1; StoreLoad; Load2

保證Store1的數據在其他處理器前可見(如flush到記憶體)先於Load2和之後的load的數據的載入。StoreLoad Barrier能夠防止load讀取到舊數據而不是最近其他處理器寫入的數據。

幾乎近代的所有的多處理器都需要StoreLoad,StoreLoad的開銷通常是最大的,並且StoreLoad具有其他三種屏障的效果,所以StoreLoad可以當做一個通用的(但是更高開銷的)屏障。

所以,利用上述的記憶體屏障,可以實現上面表格中的重排序規則

需要的屏障第二個操作第二個操作第二個操作第二個操作
第一個操作 普通讀 普通寫 volatile讀/monitor enter volatile寫/monitor exit
普通讀       LoadStore
普通讀       StoreStore
voaltile讀/monitor enter LoadLoad LoadStore LoadLoad LoadStore
volatile寫/monitor exit     StoreLoad StoreStore

為了支持final欄位的規則,需要對final的寫入增加barrier

x.finalField = v; StoreStore; sharedRef = x;

 

插入記憶體屏障

基於上面的規則,可以在volatile欄位、synchronized關鍵字的處理上增加屏障來滿足記憶體模型的規則

  1. volatile store前插入StoreStore屏障
  2. 所有final欄位寫入後但在構造器返回前插入StoreStore
  3. volatile store後插入StoreLoad屏障
  4. 在volatile load後插入LoadLoad和LoadStore屏障
  5. monitor enter和volatile load規則一致,monitor exit 和volatile store規則一致。

HappenBefore

前面提到的各種記憶體屏障對應開發者來說還是比較複雜底層,因此JMM又可以使用一系列HappenBefore的偏序關係的規則方式來說明,要想保證執行操作B的線程看到操作A的結果(無論A和B是否在同一個線程中執行), 那麼在A和B之間必須要滿足HappenBefore關係,否則JVM可以對它們任意重排序。

HappenBefore規則列表

HappendBefore規則包括

  1. 程式順序規則: 如果程式中操作A在操作B之前,那麼同一個線程中操作A將在操作B之前進行
  2. 監視器鎖規則: 在監視器鎖上的鎖操作必須在同一個監視器鎖上的加鎖操作之前執行
  3. volatile變數規則: volatile變數的寫入操作必須在該變數的讀操作之前執行
  4. 線程啟動規則: 線上程上對Thread.start的調用必須在該線程中執行任何操作之前執行
  5. 線程結束規則: 線程中的任何操作都必須在其他線程檢測到該線程已經結束之前執行
  6. 中斷規則: 當一個線程在另一個線程上調用interrupt時,必須在被中斷線程檢測到interrupt之前執行
  7. 傳遞性: 如果操作A在操作B之前執行,並且操作B在操作C之前執行,那麼操作A在操作C之前執行。

其中顯示鎖與監視器鎖有相同的記憶體語義,原子變數與volatile有相同的記憶體語義。鎖的獲取和釋放、volatile變數的讀取和寫入操作滿足全序關係,所以可以使用volatile的寫入在後續的volatile的讀取之前進行。

可以利用上述HappenBefore的多個規則進行組合。

例如線程A進入監視器鎖後,在釋放監視器鎖之前的操作根據程式順序規則HappenBefore於監視器釋放操作,而監視器釋放操作HappenBefore於後續的線程B的對相同監視器鎖的獲取操作,獲取操作HappenBefore與線程B中的操作。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 及最近部署TP5遇到了很多坑,各種環境下都會出現一些問題,下麵是我記錄的排坑之路 先說最簡單的lnmp一鍵安裝包,我用的是1.5穩定版 安裝命令:wget http://soft.vpser.net/lnmp/lnmp1.5.tar.gz -cO lnmp1.5.tar.gz && tar zxf ...
  • 說起來做一個支付系統最基礎的就是支付功能了,對於我們來說除了各大銀行以外微信和支付寶也是必選項,畢竟人家這個龐大的用戶群在那裡擺著呢,你不用那不是想著放棄這些用戶麽。 今天我們就來看一看對於我們開發者來說如何快速的進行接入。 首先我們要做的就是先去螞蟻金服開放平臺註冊賬號https://open.a ...
  • 這篇文章主要介紹了Python異常處理總結,需要的朋友可以參考下本文較為詳細的羅列了Python常見的異常處理,供大家參考,具體如下: 1.入門讀物 2.進階讀物 3.Web框架 4.爬蟲開發 5.圖形圖像6.數據分析 7.機器學習等等資料!需要的可以加QQ群:832339352!進群免費獲取! 拋 ...
  • Description George took sticks of the same length and cut them randomly until all parts became at most 50 units long. Now he wants to return sticks to ...
  • 1、Java支持基於流的通信和基於包的通信 基於流的通信使用TCP協議(傳輸控制協議)進行數據傳輸,傳輸是無損可靠的 基於包的通信使用UDP協議(用戶數據報協議)進行數據傳輸,不能保證傳輸沒有丟失 2、服務端和客戶端套接字--基於流的通信 客戶端代碼 如果服務端的埠服務沒有起來,運行客戶端的程式會 ...
  • '''re模塊 內部實現不是Python 而是調用了c的庫 re是什麼 正則 表達 式子 就是一些帶有特殊含義的符號或者符號的組合 作用: 對字元串進行過濾 在一對字元串中找到所關心的內容 你就需要告訴電腦過濾規則是什麼樣 通過什麼方式來告訴電腦 就通過正則表達式 re模塊常用方法findall ...
  • 最近發現一個網站www.unsplash.com ( 沒有廣告費哈,純粹覺得不錯 ),網頁做得很美觀,上面也都是一些免費的攝影照片,覺得很好看,就決定利用蹩腳的技能寫個爬蟲下載圖片。 先隨意感受一下這個網站: 接下來開始對網頁進行解析: 在該網頁檢查元素,選擇其中一張圖片查看它的代碼 可以看到,圖片 ...
  • 教材:《彙編語言》 王爽 第三版 Charpter 1. 基礎知識 1.1 : 機器語言 1.2 : 彙編語言的產生 1.3 : 彙編語言的組成 1.4 : 存儲器 1.5 : 指令和數據 1.6 : 存儲單元 1.7 : CPU對於存儲器的讀寫 1.8 : 地址匯流排 1.9 : 數據匯流排 1.10 ...
一周排行
    -Advertisement-
    Play Games
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...