輕鬆學JVM(二)——記憶體模型、可見性、指令重排序

来源:http://www.cnblogs.com/leefreeman/archive/2017/08/14/7356030.html
-Advertisement-
Play Games

上一篇我們介紹了JVM的基本運行流程以及記憶體結構,對JVM有了初步的認識,這篇文章我們將根據JVM的記憶體模型探索java當中變數的可見性以及不同的java指令在併發時可能發生的指令重排序的情況。 記憶體模型 首先我們思考一下一個java線程要向另外一個線程進行通信,應該怎麼做,我們再把需求明確一點,一 ...


    上一篇我們介紹了JVM的基本運行流程以及記憶體結構,對JVM有了初步的認識,這篇文章我們將根據JVM的記憶體模型探索java當中變數的可見性以及不同的java指令在併發時可能發生的指令重排序的情況。

記憶體模型

    首先我們思考一下一個java線程要向另外一個線程進行通信,應該怎麼做,我們再把需求明確一點,一個java線程對一個變數的更新怎麼通知到另外一個線程呢?我們知道java當中的實例對象、數組元素都放在java堆中,java堆是線程共用的。(我們這裡把java堆稱為主記憶體),而每一個線程都是自己私有的記憶體空間(稱為工作記憶體),如果線程1要向線程2通信,一定會經過類似的流程:

clip_image002

1、 線程1將自己工作記憶體中的X更新為1並刷新到主記憶體中;

2、 線程2從主記憶體讀取變數X=1,更新到自己的工作記憶體中,從而線程2讀取的X就是線程1更新後的值。

從上面的流程看出線程之間的通信都需要經過主記憶體,而主記憶體與工作記憶體的交互,則需要Java記憶體模型(JMM)來管理器。下圖演示了JMM如何管理主記憶體和工作記憶體:

clip_image004

當線程1需要將一個更新後的變數值刷新到主記憶體中時,需要經過兩個步驟:

1、 工作記憶體執行store操作;

2、 主記憶體執行write操作;

完成這兩步即可將工作記憶體中的變數值刷新到主記憶體,即線程1工作記憶體和主記憶體的變數值保持一致;

當線程2需要從主記憶體中讀取變數的最新值時,同樣需要經過兩個步驟:

1、主記憶體執行read操作,將變數值從主記憶體中讀取出來;

2、工作記憶體執行load操作,將讀取出來的變數值更新到本地記憶體的副本;

完成這兩步,線程2的變數和主記憶體的變數值就保持一致了。

可見性

    Java中有一個關鍵字volatile,它有什麼用呢?這個答案其實就在上述java線程間通信機制中,我們想象一下,由於工作記憶體這個中間層的出現,線程1和線程2必然存在延遲的問題,例如線程1在工作記憶體中更新了變數,但還沒刷新到主記憶體,而此時線程2獲取到的變數值就是未更新的變數值,又或者線程1成功將變數更新到主記憶體,但線程2依然使用自己工作記憶體中的變數值,同樣會出問題。不管出現哪種情況都可能導致線程間的通信不能達到預期的目的。例如以下例子:

//線程1 boolean stop = false; while(!stop){ doSomething(); } //線程2

stop 
= true;

這個經典的例子表示線程2通過修改stop的值,控制線程1中斷,但在真實環境中可能會出現意想不到的結果,線程2在執行之後,線程1並沒有立刻中斷甚至一直不會中斷。出現這種現象的原因就是線程2對線程1的變數更新無法第一時間獲取到。

但這一切等到Volatile出現後,再也不是問題,Volatile保證兩件事:

1、 線程1工作記憶體中的變數更新會強制立即寫入到主記憶體;

2、 線程2工作記憶體中的變數會強制立即失效,這使得線程2必須去主記憶體中獲取最新的變數值。

所以這就理解了Volatile保證了變數的可見性,因為線程1對變數的修改能第一時間讓線程2可見。

指令重排序

關於指令排序我們先看一段代碼:

int a = 0;
boolean flag = false;

//線程1

public void writer() {

a = 1;

flag = true;

}

//線程2

public void reader() {

if (flag) {

int i= a+1;

...... }

}

線程1依次執行a=1,flag=true;線程2判斷到flag==true後,設置i=a+1,根據代碼語義,我們可能會推斷此時i的值等於2,因為線程2在判斷flag==true時,線程1已經執行了a=1;所以i的值等於a+1=1+1=2;但真實情況卻不一定如此,引起這個問題的原因是線程1內部的兩條語句a=1;flag=true;可能被重新排序執行,如圖:

clip_image006

這就是指令重排序的簡單演示,兩個賦值語句儘管他們的代碼順序是一前一後,但真正執行時卻不一定按照代碼順序執行。你可能會說,有這個指令重排序那不是亂套了嗎?我寫的程式都不按我的代碼流程走,這怎麼玩?這個你可以放心,你的程式不會亂套,因為java和CPU、記憶體之間都有一套嚴格的指令重排序規則,哪些可以重排,哪些不能重排都有規矩的。下列流程演示了一個java程式從編譯到執行會經歷哪些重排序:

clip_image008

在這個流程中第一步屬於編譯器重排查,編譯器重排序會按JMM的規範嚴格進行,換言之編譯器重排序一般不會對程式的正確邏輯造成影響。第二、三步屬於處理器重排序,處理器重排序JMM就不好管了,怎麼辦呢?它會要求java編譯器在生成指令時加入記憶體屏障,記憶體屏障是什麼?你可以理解為一個不透風的保護罩,把不能重排序的java指令保護起來,那麼處理器在遇到記憶體屏障保護的指令時就不會對它進行重排序了。關於在哪些地方該加入記憶體屏障,記憶體屏障有哪些種類,各有什麼作用,這些知識點這裡就不再闡述了。可以參考JVM規範相關資料。

下麵介紹一下在同一個線程中,不會被重排序的邏輯:

clip_image010

這三種情況中,任意改變一個代碼的順序,結果都會大不相同,對於這樣的邏輯代碼,是不會被重排序的。註意這是指單線程中不會被重排序,如果在多線程環境下,還是會產生邏輯問題,例如我們一開始舉的例子。

結語

本文簡單介紹了java在實現線程間通信時的簡單原理,並介紹了volatile關鍵字的作用,最後介紹了java當中可能會出現指令重排序的情況。下一篇將介紹JVM中的參數設置對java程式的影響。

 

參考資料:

《實戰Java虛擬機》 葛一鳴

《深入理解Java虛擬機(第2版)》 周志明

《深入理解Java記憶體模型》 程曉明


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

-Advertisement-
Play Games
更多相關文章
  • 題目背景 令 夜 色 的 鐘 聲 響 起 令 黃 昏 (起 始) 的 鐘 聲 響 起 我 愛 (渴 望) 的 就 只 有 你 我 愛 ( 渴 望 ) 你 正因如此 獨自安靜地哭泣吧 正因如此 無論你在何處哭泣 我都會率先去迎接你 不存在何處 直至深夜(小小的你) 你存在此處 至美者(心顯崇高之人) ...
  • file類常用方法 delete()刪除此抽象路徑名錶示的文件和目錄。 equals()測試此抽象路徑名與給定對象是否相等。 exists()測試此抽象路徑名錶示的文件或目錄是否存在。 getName()返回由此抽象路徑名錶示的文件或目錄的名稱。 isDirectory()測試此抽象路徑名錶示的文件 ...
  • 題目背景 縮點+DP 題目描述 給定一個n個點m條邊有向圖,每個點有一個權值,求一條路徑,使路徑經過的點權值之和最大。你只需要求出這個權值和。 允許多次經過一條邊或者一個點,但是,重覆經過的點,權值只計算一次。 輸入輸出格式 輸入格式: 第一行,n,m 第二行,n個整數,依次代表點權 第三至m+2行 ...
  • 逆向工程1.什麼是逆向工程mybaits需要程式員自己編寫sql語句,mybatis官方提供逆向工程 可以針對單表自動生成mybatis執行所需要的代碼(mapper.java,mapper.xml、po..)企業實際開發中,常用的逆向工程方式:由於資料庫的表生成java代碼。2.下載逆向工程myb ...
  • 巨集觀上: 1.技術廣度方面至少要精通多門開源技術吧,研究過struts\spring等的源碼。2.項目經驗方面從頭到尾跟過幾個大項目,頭是指需求階段,包括需求調研。尾是指上線交付之後,包括維護階段。3.架構經驗方面有過分散式系統的架構和開發經驗。對於跨系統的結構優化,數據存儲的性能指標等有豐富經驗。 ...
  • Java的三大版本是什麼?它們有什麼功能?Java另一個與三有關的三大環境是什麼?它們是什麼關係?併列還是包含?Oracle公司官網UI更新後,Java又怎麼下載?小星星帶你一探究竟。 ...
  • 本篇文章講解的是在centos7.3下配置 Apache2.4 + MySQL5.7 + PHP7.1.8 一.Apache 1.查看httpd包是否可用yum list | grep httpd 2.安裝Apacheyum install httpd 3.配置servernamevi /etc/h ...
  • 上一章節,我們講解了通過mybatis的懶載入來提高查詢效率,那麼除了懶載入,還有什麼方法能提高查詢效率呢?這就是我們本章講的緩存。 mybatis 為我們提供了一級緩存和二級緩存,可以通過下圖來理解: ①、一級緩存是SqlSession級別的緩存。在操作資料庫時需要構造sqlSession對象,在 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...