什麼是JAVA記憶體模型

来源:https://www.cnblogs.com/waldron/archive/2022/10/30/16842604.html
-Advertisement-
Play Games

前言 在併發編程中,當多個線程同時訪問同一個共用的可變變數時,會產生不確定的結果,所以要編寫線程安全的代碼,其本質上是對這些可變的共用變數的訪問操作進行管理。導致這種不確定結果的原因就是可見性、有序性和原子性問題,Java 為解決可見性和有序性問題引入了 Java 記憶體模型,使用互斥方案(其核心實現 ...


前言

在併發編程中,當多個線程同時訪問同一個共用的可變變數時,會產生不確定的結果,所以要編寫線程安全的代碼,其本質上是對這些可變的共用變數的訪問操作進行管理。導致這種不確定結果的原因就是可見性有序性原子性問題,Java 為解決可見性和有序性問題引入了 Java 記憶體模型,使用互斥方案(其核心實現技術是)來解決原子性問題。這篇先來看看解決可見性、有序性問題的 Java 記憶體模型(JMM)。

什麼是 Java 記憶體模型

Java 記憶體模型在維基百科上的定義如下:

The Java memory model describes how threads in the Java programming language interact through memory. Together with the description of single-threaded execution of code, the memory model provides the semantics of the Java programming language.

記憶體模型限制的是共用變數,也就是存儲在堆記憶體中的變數,在 Java 語言中,所有的實例變數、靜態變數和數組元素都存儲在堆記憶體之中。而方法參數、異常處理參數這些局部變數存儲在方法棧幀之中,因此不會線上程之間共用,不會受到記憶體模型影響,也不存在記憶體可見性問題。

通常,線上程之間的通訊方式有共用記憶體和消息傳遞兩種,很明顯,Java 採用的是第一種即共用的記憶體模型,在共用的記憶體模型里,多線程之間共用程式的公共狀態,通過讀-寫記憶體的方式來進行隱式通訊。

從抽象的角度來看,JMM 其實是定義了線程和主記憶體之間的關係,首先,多個線程之間的共用變數存儲在主記憶體之中,同時每個線程都有一個自己私有的本地記憶體,本地記憶體中存儲著該線程讀或寫共用變數的副本(註意:本地記憶體是 JMM 定義的抽象概念,實際上並不存在)。抽象模型如下圖所示:

image-20221030222730497

在這個抽象的記憶體模型中,在兩個線程之間的通信(共用變數狀態變更)時,會進行如下兩個步驟:

  1. 線程 A 把在本地記憶體更新後的共用變數副本的值,刷新到主記憶體中。
  2. 線程 B 在使用到該共用變數時,到主記憶體中去讀取線程 A 更新後的共用變數的值,並更新線程 B 本地記憶體的值。

JMM 本質上是在硬體(處理器)記憶體模型之上又做了一層抽象,使得應用開發人員只需要瞭解 JMM 就可以編寫出正確的併發代碼,而無需過多瞭解硬體層面的記憶體模型。

為什麼需要 Java 記憶體模型

在日常的程式開發中,為一些共用變數賦值的場景會經常碰到,假設一個線程為整型共用變數 count 做賦值操作(count = 9527;),此時就會有一個問題,其它讀取該共用變數的線程在什麼情況下獲取到的變數值為 9527 呢?如果缺少同步的話,會有很多因素導致其它讀取該變數的線程無法立即甚至是永遠都無法看到該變數的最新值。

比如緩存就可能會改變寫入共用變數副本提交到主記憶體的次序,保存在本地緩存的值,對於其它線程是不可見的;編譯器為了優化性能,有時候會改變程式中語句執行的先後順序,這些因素都有可能會導致其它線程無法看到共用變數的最新值。

在文章開頭,提到了 JMM 主要是為瞭解決可見性有序性問題,那麼首先就要先搞清楚,導致可見性有序性問題發生的本質原因是什麼?現在的服務絕大部分都是運行在多核 CPU 的伺服器上,每顆 CPU 都有自己的緩存,這時 CPU 緩存與記憶體的數據就會有一致性問題了,當一個線程對共用變數的修改,另外一個線程無法立刻看到。導致可見性問題的本質原因是緩存

image-20221030222814241

有序性是指代碼實際的執行順序和代碼定義的順序一致,編譯器為了優化性能,雖然會遵守 as-if-serial 語義(不管怎麼重排序,在單線程下的執行結果不能改變),不過有時候編譯器及解釋器的優化也可能引發一些問題。比如:雙重檢查來創建單實例對象。下麵是使用雙重檢查來實現延遲創建單例對象的代碼:

public class DoubleCheckedInstance {

  private static DoubleCheckedInstance instance;

  public static DoubleCheckedInstance getInstance() {
    if (instance == null) {
      synchronized (DoubleCheckedInstance.class) {
        if (instance == null) {
          instance = new DoubleCheckedInstance();
        }
      }
    }

    return instance;
  }
  
}

這裡的 instance = new DoubleCheckedInstance();,看起來 Java 代碼只有一行,應該是無法就行重排序的,實際上其編譯後的實際指令是如下三步:

  1. 分配對象的記憶體空間
  2. 初始化對象
  3. 設置 instance 指向剛剛已經分配的記憶體地址

上面的第 2 步和第 3 步如果改變執行順序也不會改變單線程的執行結果,也就是說可能會發生重排序,下圖是一種多線程併發執行的場景:

image-20221030222909517

此時線程 B 獲取到的 instance 是沒有初始化過的,如果此來訪問 instance 的成員變數就可能觸發空指針異常。導致有序性問題的本質原因是編譯器優化。那你可能會想既然緩存和編譯器優化是導致可見性問題和有序性問題的原因,那直接禁用掉不就可以徹底解決這些問題了嗎,但是如果這麼做了的話,程式的性能可能就會受到比較大的影響了。

其實可以換一種思路,能不能把這些禁用緩存和編譯器優化的權利交給編碼的工程師來處理,他們肯定最清楚什麼時候需要禁用,這樣就只需要提供按需禁用緩存和編譯優化的方法即可,使用比較靈活。因此Java 記憶體模型就誕生了,它規範了 JVM 如何提供按需禁用緩存和編譯優化的方法,規定了 JVM 必須遵守一組最小的保證,這個最小保證規定了線程對共用變數的寫入操作何時對其它線程可見。

順序一致性記憶體模型

順序一致性模型是一個理想化後的理論參考模型,處理器和編程語言的記憶體模型的設計都是參考的順序一致性模型理論。其有如下兩大特性:

  1. 一個線程中的所有操作必須按照程式的順序來執行
  2. 所有的線程都只能看到一個單一的執行操作順序,不管程式是否同步

在工程師視角下的順序一致性模型如下:

image-20221030222940954

順序一致性模型有一個單一的全局記憶體,這個全局記憶體可以通過左右搖擺的開關可以連接到任意一個線程,每個線程都必須按照程式的順序來執行記憶體的讀和寫操作。該理想模型下,任務時刻都只能有一個線程可以連接到記憶體,當多個線程併發執行時,就可以通過開關就可以把多個線程的讀和寫操作串列化

順序一致性模型中,所有操操作完全按照順序串列執行,但是在 JMM 中就沒有這個保證了,未同步的程式在 JMM 中不僅程式的執行順序是無序的,而且由於本地記憶體的存在,所有線程看到的操作順序也可能會不一致,比如一個線程把寫共用變數保存在本地記憶體中,在還沒有刷新到主記憶體前,其它線程是不可見的,只有更新到主記憶體後,其它線程才有可能看到。

JMM 對在正確同步的程式做了順序一致性的保證,也就是程式的執行結果和該程式在順序一致性記憶體模型中的執行結果相同。

Happens-Before 規則

Happens-Before 規則是 JMM 中的核心概念,Happens-Before 概念最開始在 這篇論文 提出,其在論文中使用 Happens-Before 來定義分散式系統之間的偏序關係。在 JSR-133 中使用 Happens-Before 來指定兩個操作之間的執行順序。

JMM 正是通過這個規則來保證跨線程的記憶體可見性,Happens-Before 的含義是前面一個對共用變數的操作結果對該變數的後續操作是可見的,約束了編譯器的優化行為,雖然允許編譯器優化,但是優化後的代碼必須要滿足 Happens-Before 規則,這個規則給工程師做了這個保證:同步的多線程程式是按照 Happens-Before 指定的順序來執行的。目的就是為了在不改變程式(單線程或者正確同步的多線程程式)執行結果的前提下,盡最大可能的提高程式執行的效率

image-20221030223015933

JSR-133 規範中定瞭如下 6 項 Happens-Before 規則:

  1. 程式順序規則:一個線程中的每個操作,Happens-Before 該線程中的任意後續操作
  2. 監視器鎖規則:對一個鎖的解鎖操作,Happens-Before 於後面對這個鎖的加鎖操作
  3. volatile 規則對一個 volatile 類型的變數的寫操作,Happens-Before 與任意後面對這個 volatile 變數的讀操作
  4. 傳遞性規則:如果操作 A Happens-Before 於操作 B,並且操作 B Happens-Before 於操作 C,則操作 A Happens-Before 於操作 C
  5. start() 規則:如果一個線程 A 執行操作 threadB.start() 啟動線程 B,那麼線程 A 的 start() 操作 Happens-Before 於線程 B 的任意操作
  6. join() 規則:如果線程 A 執行操作 threadB.join() 併成功返回,那麼線程 B 中的任意操作 Happens-Before 於線程 A 從 threadB.join() 操作成功返回

JMM 的一個基本原則是:只要不改變單線程和正確同步的多線程的執行結果,編譯器和處理器隨便怎麼優化都可以,實際上對於應用開發人員對於兩個操作是否真的被重排序並不關心,真正關心的是執行結果不能被修改。因此 Happens-Before 本質上和 sa-if-serial 的語義是一致的,只是 sa-if-serial 只是保證在單線程下的執行結果不被改變。

總結

本文主要介紹了記憶體模型的相關基礎知識和相關概念,JMM 屏蔽了不同處理器記憶體模型之間的差異,在不同的處理器平臺上給應用開發人員抽象出了統一的 Java 記憶體模型(JMM)。常見的處理器記憶體模型比 JMM 的要弱,因此 JVM 會在生成位元組碼指令時在適當的位置插入記憶體屏障(記憶體屏障的類型會因處理器平臺而有所不同)來限制部分重排序。

轉載自併發編程網 – ifeve.com本文鏈接地址: Java 記憶體模型


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

-Advertisement-
Play Games
更多相關文章
  • 相信用過vue的小伙伴,肯定被面試官問過這樣一個問題:在vue中動態的引入圖片為什麼要使用require 有些小伙伴,可能會輕蔑一笑:呵,就這,因為動態添加src被當做靜態資源處理了,沒有進行編譯,所以要加上require, 我倒著都能背出來...... emmm... 乍一看好像說的很有道理啊... ...
  • 通過 JavaScript 在瀏覽器中獲取或設置剪貼板中的內容,常用於一鍵複製或使用網頁油猴複製限制文本 使用 ~~execCommand~~ (已棄用) 寫入文本到剪貼板 document.onclick = function() { let text = 'hello world' let do ...
  • 前言 最近無聊看直播,虎牙廣告是真多,還有一堆ghs直播間經常出現在首頁,不想看到這些直播間,於是想辦法屏蔽直播間。 源碼地址 插件地址 演示 下麵先看看未安裝插件之前 虎牙首頁一堆廣告,視頻自動播放 更過分是一堆ghs,噁心人玩意兒的出現在這裡,真是影響觀看體驗 還有左側推薦欄也很煩 安裝之後: ...
  • 1.4 超鏈接 1.4.1 基礎語法 基礎語法: <a href="網頁地址"> </a> 拓展參數: <a href="網頁地址" target="跳轉方式"> </a> | href | 跳轉鏈接地址 | | | | | target | 鏈接打開方式 | 1.4.2 錨鏈接(id參數) 每個標 ...
  • 為了提高系統吞吐率,也就是提高生產效率,核心觀點如下,系統設計也是如此 在微服務或任何其他基於事件的架構(event-driven-architecture)中,在一些用例中,一個服務可能需要我們對他們自己的本地資料庫進行修改,同時發佈一個事件。然後,該事件會被其他服務所消費。為了擁有一個一致的軟體 ...
  • 您好,我是湘王,這是我的博客園,歡迎您來,歡迎您再來~ 之前說過,AQS(抽象隊列同步器)是Java鎖機制的底層實現。既然它這麼優秀,是騾子是馬,就拉出來溜溜吧。 首先用重入鎖來實現簡單的累加,就像這樣: /** * 用重入鎖實現累加 * * @author 湘王 */ public class M ...
  • 前提 java version "1.8.0_25" 池簡述 軟體開發活動中,我們經常會聽到資料庫連接池、記憶體池、線程池等各種“池”概念,這些“池”到底是什麼東西呢?程式的世界里,我們可以將池簡單的理解為一種容器類數據結構,比如列表。程式處理信息的過程中,可能會依賴某些資源或者對象(暫且統一稱之為對 ...
  • 概念: 責任鏈模式又叫做職責鏈模式,是屬於行為型的一種。 責任鏈模式分為兩種: 第一種像工廠流水線,目的就讓每一個環節都進行處理。 第二種像層層審批,如果其中一個對象無法處理,調用下一個對象來進行處理,如果一旦能處理,鏈式就此停止,以此類推,第二種為了程式的健壯性,至少得需要一個兜底的模塊,防止穿透 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...