Java的記憶體模型

来源:https://www.cnblogs.com/wadmwz/archive/2018/08/07/9430279.html
-Advertisement-
Play Games

Java的記憶體模型 概念 Java記憶體模型的主要目標是定義程式中各個變數的訪問規則,即在虛擬機中將變數存儲到記憶體中取出變數(這的變數包括實例欄位。靜態欄位和構成數組對象的元素)這樣的底層細節。 為了獲得較好的執行效能。Java記憶體模型沒有限制執行引擎使用處理器的特定寄存器或緩存來和主記憶體進行交互,也 ...


Java的記憶體模型

概念

Java記憶體模型的主要目標是定義程式中各個變數的訪問規則,即在虛擬機中將變數存儲到記憶體中取出變數(這的變數包括實例欄位。靜態欄位和構成數組對象的元素)這樣的底層細節。

為了獲得較好的執行效能。Java記憶體模型沒有限制執行引擎使用處理器的特定寄存器或緩存來和主記憶體進行交互,也沒有限制即時編譯器進行調整代碼執行順序這類優化措施。

Java記憶體模型規定了所有的變數(前面的變數)都存儲在主記憶體(Main Memory)中,每條線程還有自己的工作記憶體(Working Memory),線程的工作記憶體中保存了被該線程使用到的變數的主記憶體副本拷貝,線程對變數的所有操作(讀取,賦值等等)都必須在工作記憶體中進行,而不能直接讀寫主記憶體中的變數,不同的線程之間也無法直接訪問對方工作記憶體中的變數,線程間變數的傳遞都依靠記憶體完成。線程、主記憶體、工作記憶體的關係如圖:

主記憶體和工作記憶體之間的交互協議(變數拷貝到工作記憶體,再從工作記憶體同步回主記憶體),Java記憶體模型定義了8種操作來完成,虛擬機實現必須保證每一個操作都是原子性、不可再分的(double和long例外)。

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

變數從主記憶體到工作記憶體,必須經過read和load,從 工作記憶體同步回主記憶體,需要經過store和write操作,這兩個操作必須按順序執行,但不一定是連續執行。也就是在兩個操作之間可以有其他的操作。 在使用上面的8中操作的時候需要滿足的規則 :

* 不允許read和load、 store和 write操作之一單獨出現,即不允許一個變數從主記憶體讀取了但工作記憶體不接受,或者從工作存發起回寫了但主記憶體不接受的情況出現。
* 不允許一個線程丟棄它的最近的 assign操作,即變數在工作記憶體中改變了之後必須把該變化同步回主記憶體。
* 不允許一個線程無原因地(沒有發生過任何 assign操作)把數據從線程的工作記憶體同步回主記憶體中。
* 一個新的變數只能在主記憶體中“誕生”,不允許在工作記憶體中直接使用一個未被初始化(load或 assign)的變數,換句話說,就是對一個變數實施use、 store操作之前必須先執行過了 assign和load操作。
* 一個變數在同一個時刻只允許一條線程其進行1ock操作,但lock操作可以被同一條線程重覆執行多次,多次執行lock後,只有執行相同次數的 unlock操作,變數被解鎖。
* 如果對一個變數執行lock操作,那將會清空工作記憶體中此變數的值,在執行引擎使用這個變數前,需要重新執行load或 assign操作初始化變數的值。
* 如果一個變數事先沒有被lock操作鎖定,那就不允許對它執行unlock操作,也不允許去 unlock一個被其他線程鎖定住的變數。
* 對一個變數執行 unlock操作之前,必須先把此變數同步回主記憶體中(執行stre、wite操作)。

Java記憶體模型要求的8個操作都具有原子性,但是對於64位的數據類型(long和double),定義了寬鬆的規定:允許虛擬機將沒有被volitale修飾的64位數據的讀寫操作劃分為兩次32位的操作來進行,也就是虛擬機實現選擇可以不保證64位數據類型的load、store、read和write這四個操作的原子性,這個就是所謂的long和double的非原子性協定。

記憶體模型的介紹

重排序

如果兩個操作訪問同一個變數,且這兩個操作中有一個為寫操作,此時這兩個操作之間就存在數據依賴性。編譯器和處理器可能會對操作做重排序,但是不會改變存在數據依賴關係的兩個操作的執行順序。遵循as-if-serial語義:即不管程式怎麼重排序(編譯器和處理器為了提高並行度),(單線程)程式的執行結果不能被改變,但在多線程中,對存在控制依賴的操作重排序,可能會改變程式的執行結果。

volatile型變數的規則

volatile是Java虛擬機提供的最輕量級的同步機制。當一個變數被定義為volatile類型後具備兩種特性 :第一是保證此變數對所有線程的可見性,這裡的“可見性”是指當一條線程改了這個變數的值,新值對於其他線程來說是可以立即得知的。而普通變數不能做到這一點,普通變數的值線上程間傳遞均需要通過主記憶體來完成,例如,線程A修改一個普通變數的值,然後向主記憶體進行回寫。 第二是一條線程B線上程A回寫完成了之後再從主記憶體進行讀取操作,新變數值才會對線程B可見。

使用volatile變數的第一個語義是只保證可見性,有些場景下不適用,這個時候仍然需要通過加鎖來保證原子性。不符合的運算場景:一是運算結果並不依賴變數的當前值,或者能夠確保只有單一的線程修改變數的值。二是變數不需要與其他的狀態變數共同參與不變約束。

第二個語義是禁止指令重排序優化,前面的重排序優化說過,編譯器和處理器為了提高並行度,會對程式進行重排序,不能保證變數賦值的操作順序與程式代碼中的執行順序一致。在使用volatile之後,在進行對volatile修飾的變數進行賦值後,會多執行一個lock操作,這個操作相當於一個記憶體屏障(Memory Barrier或Memory Fence,指重排序時不能把後面的指令重排序到記憶體屏障之前的位置),只有一個CPU訪問是不需要這個記憶體屏障的,但是多個CPU 訪問同一塊記憶體,且其中一個在觀測另一個,就需要記憶體屏障來保證一致性了。

Java記憶體模型中對 volatile變數定義的特殊規則。假定T表示一個線程,V和W分別表示兩個 volatile型變數,那麼在進行read、load、use、 assign store和 write操作時需要滿足如下規則:

* 只有當線程T對變數V執行的前一個作是load的時候,線程T才能對變數V執行use動作;並且,只有當線程T對變數V執行的後一個動作是use的時候,線程T才能對變數V執行load動作。線程T對變數V的use動作可以認為是和線程T對變數V的load、read動作相關聯,必須連續一起出現(這條規則要求在工作記憶體中,每次使用v前都必須先從主記憶體刷新最新的值,用於保證能看見其他線程對變數V所做的修改後的值)。
* 只有當線程T對變數V執行的前一個動作是assign的時候,線程T才能對變數V執行store動作;並且,只有當線程T對變數V執行的後一個動作是store的時候,線程T才能對變數V執行assign動作。線程T對變數V的assign動作可以認為是和線程T對變數V的store、 write動作相關聯,必須連續一起出現(這條規則要求在工作記憶體中每次修改V後都必須立刻同步回主記憶體中,用於保證其他線程可以看到自己對變數v所做的修改)。
* 假定動作A是線程T對變數V實施的use或assign動作,假定動作F是和動作A相關聯的load或 store動作,假定動作P是和動作F相應的對變數V的read或 wrte動作;類似的,假定動作B是線程T對變數W實施的use或 assign動作,假定動作是和動作B相關聯的load或 store動作,假定動作Q是和動作G相應的對變數WErad或 write a動作。如果A先於B,那麼P先於Q(這條規則要求 volatile 修飾的變數不會被指令重排序優化,保證代碼的執行順序與程式的順序相同)。

原子性

Java記憶體模型來直接保證的原子性變數操作包括read、load、assign, use、 store和 wrte,我們大致可以認為基本數據類型的訪問讀寫是具備原子性的(例外就是long和 double的非原子性協定),如果應用場景需要一個更大範圍的原子性保證,Java記憶體模型還提供lock和 unlock操作來滿足這種需求,儘管虛擬機未把lock和 unlock操作直接開放給用戶使用,但是卻提供了更高層次的位元組碼指令 monitorenter和 monitorexit來隱式地使用這兩個操作,這兩個位元組碼指令反映到Java代碼中就是同步塊— synchronized關鍵字,因此在synchronized塊之間的操作也具備原子性。

可見性

可見性是指當一個線程修改了共用變數的值,其他線程能夠立即得知這個修改。前面說 volatile變數的時候說過這一點。Java記憶體模型是通過在變數修改後將新值同步回主記憶體,在變數讀取前從主記憶體刷新變數值。這種依賴主記憶體作為傳遞媒介的方式來實現可見性,無論是普通變數還是 volatile變數都是如此,普通變數與volatile變數的區別是, volatile的特殊規則保證了新值能立即同步到主記憶體,以及每次使用前立即從主記憶體刷新。因此,可以說 volatile保證了多線程操作時變數的可見性,而普通變數則不能保證這一點。

除了 volatile之外,Java還有兩個關鍵字能實現可見性,即 synchronized和 final。同步塊的可見性是由“對一個變數執行 unlock作之前,必須先把此變數同步回主記憶體中(執行store、 write操作)”這條規則獲得的,而final關鍵字的可見性是指:被 final修飾的欄位在構造器中一旦初始化完成,並且構造器沒有把“this”的引用傳遞出去(this引用逃逸是一件很危險的事情,其他線程有可能通過這個引用訪問到“初始化了一半”的對象),那在其他線程中就能看見 final欄位的值。

有序性

Java記憶體模型的有序性在前面說volatile時也說過了,Java程式中天然的有序性可以總結為一句話:如果在本線程內觀察,所有的操作都是有序的;如果在一個線程中觀察另一個線程,所有的操作都是無序的。前半句是指“線程內表現為串列的語義”( Within- Thread As-If- Serial Semantics),後半句是指“指令重排序”現象和“工作記憶體與主記憶體同步延遲”現象。

Java語言提供了 volatile和 synchronized 兩個關鍵字來保證線程之間操作的有序性 ,volatile關鍵字本身就包含了禁止指令重排序的語義,而 synchronized則是由“一個變數在同個時刻只允許一條線程對其進行lock操作”這條規則獲得的,這條規則決定了持有同一個鎖的兩個同步塊只能串列地進入。

先行發生原則

先行發生是Java記憶體模型中定義的兩項操作之間的偏序關係,如果說操作A先行發生於操作B,其實就是說在發生操作B之前,操作A產生的影響能被操作B觀察到,“影響”包括修改了記憶體中共用變數的值、發送了消息、調用了方法等。

下麵是Java記憶體模型下一些“天然的”先行發生關係,這些先行發生關係無須任何同步協助就已經存在,可以在編碼中直接使用。如果兩個操作之間的關係不在此列,並且無法下列規則推導出來的話,它們就沒有順序性保障,虛擬機可以對它們隨意地進行重排序 :

  • 程式次序規則( Program Order Rule):在一個線程內,按照程式代碼順序,書寫在前面的操作先行發生於書寫在後面的操作。準確地說,應該是控制流順序而不是程式代碼順序,因為要考慮分支、迴圈等結構。
  • 管程鎖定規則( Monitor Lock Rule):一個 unlock操作先行發生於後面對同一個鎖的lock操作。這裡必須強調的是同一個鎖,而“後面”是指時間上的先後順序。
  • volatile變數規則( Volatile Variable Rul):對一個 volatile變數的寫操作先行發生於後面對這個變數的讀操作,這裡的“後面”同樣是指時間上的先後順序。
  • 線程啟動規則( Thread Start Rule): Thread對象的 start()方法先行發生於此線程的每一個動作。
  • 線程終止規則( Thread Termination Rul):線程中的所有操作都先行發生於對此線程的終止檢測,我們可以通過 Thread.join()方法結束、 Thread.isAlive()的返回值等手段檢測到線程已經終止執行。
  • 線程中斷規則( Thread Interruption Rul):對線程 interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,可以通過 Thread.interrupted()方法檢測到是否有中斷發生。
  • 對象終結規則( Finalizer Rule):一個對象的初始化完成(構造函數執行結束)先行發生於它的 finalize()方法的開始。
  • 傳遞性(Transitivity):如果操作A先行發生於操作B,操作B先行發生於操作C,那就可以得出操作A先行發生於操作C的結論。

在Java程式中,時間先後順序與先行發生原則之間基本沒有太大的關係,所以衡量併發安全問題的時候不要受到時間順序的干擾,一切必須以先行發生原則為準。


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

-Advertisement-
Play Games
更多相關文章
  • 不要跟產品經理打架,失控是一種病。我這裡有一劑良藥瞭解下?! ...
  • 特別提醒:一定要註意文件結構 WebappApplication 一定要在包的最外層,否則Spring無法對所有的類進行托管,會造成@Autowired 無法註入。 1. 添加工具類獲取在 Spring 中托管的 Bean (1)工具類 (2)使用 1)程式啟動時,實例化 SpringContext ...
  • 最課程學員啟示錄:這麼PL的小姐姐給你做……你要不要? 想撒呢,給你做程式媛你要不要? 一句話,先上圖,而且必須是經得住考驗的素顏無碼高清大圖身份照: 我覺得未來我們可以搞個校花評選,你們不反對的話,我先投贊成票了。 我記得小姐姐是第三期過來的,反正我不知道是不是巧合,小姐姐剛過來,二期就有好幾個同 ...
  • 在C++中所有數據組合的類型都是自定義的數據結構。 包括我們常常使用的string、istream、ostream等。 一個簡單的類型定義,以struct開頭進行書寫。 類內初始值的作用:創建對象時,類內初始值將用於初始化數據成員。 如何使用我們自定義的類型 運行結果: 以上我們的程式就已經基本完成 ...
  • python語言是目前最流行的編程語言之一,在筆者寫這篇文章的前一周,2018年的IEEE的編程語言排行出來了,python又雄踞第一。 Python 強勢霸榜第一名!排名第二的 C++ 得分是 98.4。Java 今年排名降至第四,得分為 97.5。今年排名第三的是 C,得分為 98.2。 總的來 ...
  • 1. 鏈式前向星的作用 快速,省空間地存儲圖結構。 例如一道普通圖論題,n個點,m條邊。在n較小的情況下,可以使用任意一種存圖方式。 但是n較大的時候呢?n n的鄰接矩陣顯然不能使用。 這時我們使用鏈式前向星。 2. 鏈式前向星的原理 在普通前向星中 3. 鏈式前向星的代碼 4. 鏈式前向星的使用 ...
  • DLL載入,設置相對路徑1、 載入dll方法之一:(./ 代表當前目錄,../ 代表上層目錄)包含頭文件的相對路徑(當前路徑為源代碼路徑,路徑 “../../” 當前項目文件夾上級目錄),鏈接lib文件的相對路徑(當前項目文件夾里“..\\”,這裡需要轉意字元\)。如果項目1依賴項目2(滑鼠右鍵點擊 ...
  • javaweb項目中有很多場景的路徑客戶端的POST/GET請求,伺服器的請求轉發,資源獲取需要設置路徑等這些路徑表達的含義都有不同,所以想要更好的書寫規範有用的路徑代碼 需要對路徑有一個清晰地認知 javaweb項目中有很多場景的路徑客戶端的POST/GET請求,伺服器的請求轉發,資源獲取需要設置 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...