<input id="ohw05"></input>
  • <table id="ohw05"><menu id="ohw05"></menu></table>
  • <var id="ohw05"></var>
  • <code id="ohw05"><cite id="ohw05"></cite></code>
    <label id="ohw05"></label>
    <var id="ohw05"></var>
  • 從 CPU 講起,深入理解 Java 內存模型!

    Java 內存模型,許多人會錯誤地理解成 JVM 的內存模型。但實際上,這兩者是完全不同的東西。Java 內存模型定義了 Java 語言如何與內存進行交互,具體地說是 Java 語言運行時的變量,如何與我們的硬件內存進行交互的。而 JVM 內存模型,指的是 JVM 內存是如何劃分的。

    Java 內存模型是并發編程的基礎,只有對 Java 內存模型理解較為透徹,我們才能避免一些錯誤地理解。Java 中一些高級的特性,也建立在 Java 內存模型的基礎上,例如:volatile 關鍵字。為了讓大家能明白 Java 內存模型存在的意義,本篇文章將從計算機硬件出發,一路寫到操作系統、編程語言,一環扣一環的引出 Java 內存模型存在的意義,讓大家對 Java 內存模型有較為深刻的理解。看完之后,希望大家能夠明白如下幾個問題:

    1. 為什么要有 Java 內存模型?
    2. Java 內存模型解決了什么問題?
    3. Java 內存模型是怎樣的一個東西?

    從 CPU 說起

    我們知道計算機有 CPU 和內存兩個東西,CPU 負責計算,內存負責存儲數據,每次 CPU 計算前都需要從內存獲取數據。我們知道 CPU 的運行速度遠遠快于內存的速度,因此會出現 CPU 等待內存讀取數據的情況。

    由于兩者的速度差距實在太大,我們為了加快運行速度,于是計算機的設計者在 CPU 中加了一個CPU 高速緩存。這個 CPU 高速緩存的速度介于 CPU 與內存之間,每次需要讀取數據的時候,先從內存讀取到CPU緩存中,CPU再從CPU緩存中讀取。這樣雖然還是存在速度差異,但至少不像之前差距那么大了。

    新增 CPU 高速緩存

    隨著技術的發展,多核 CPU 出現了,CPU 的計算能力進一步提高。原本同一時間只能運行一個任務,但現在可以同時運行多個任務。由于多核 CPU 的出現,雖然提高了 CPU 的處理速度,但也帶來了新的問題:緩存一致性。

    在多 CPU 系統中,每個處理器都有自己的高速緩存,而它們又共享同一主內存,如下圖所示。當多個 CPU 的運算任務都涉及同一塊主內存區域時,可能導致各自的緩存數據不一致。如果發生了這種情況,那同步回主內存時以哪個 CPU 高速緩存的數據為準呢?

    多核 CPU 及高速緩存導致的問題

    我們舉個例子,線程 A 執行這樣一段代碼:

    i = i + 10;
    

    線程 B 執行這樣一段代碼:

    i = i + 10;
    

    他們的 i 都是存儲在內存中共用的,初始值是 0。按照我們的設想,最終輸出的值應該是 20 才對。但實際上有可能輸出的值是 10。下面是可能發生的一種情況:

    • 線程 A 分配到 CPU0 執行,這時候讀取 i 的值為 0,存到 CPU0 的高速緩存中。
    • 線程 B 分配到 CPU1 執行,這時候讀取 i 的值為 0,存到 CPU1 的高速緩存中。
    • CPU0 進行運算,得出結果 10,運算結束,寫回內存,此時內存 i 的值為 10。
    • CPU1 進行運算,得出結果 10,運算結束,寫回內存,此時內存 i 的值為 10。

    可以看到發生錯誤結果的主要原因是:兩個 CPU 高速緩存中的數據是相互獨立,它們無法感知到對方的變化。

    到這里,就產生了第一個問題:硬件層面上,由于多 CPU 的存在,以及加入 CPU 高速緩存,導致的數據一致性問題。

    要注意的是,這個問題是硬件層面上的問題。只要使用了多 CPU 并且 CPU 有高速緩存,那就會遇到這個問題。對于生產該 CPU 的廠商,就需要去解決這個問題,這與具體操作系統無關,也與編程語言無關。

    那么如何解決這個問題呢?答案是:緩存一致性協議。

    加入緩存一致性協議

    所謂的緩存一致性協議,指的是在 CPU 高速緩存與主內存交互的時候,遵守特定的規則,這樣就可以避免數據一致性問題了。

    在不同的 CPU 中,會使用不同的緩存一致性協議。例如 MESI 協議用于奔騰系列的 CPU 中,而 MOSEI 協議則用于 AMD 系列 CPU 中,Intel 的 core i7 處理器使用 MESIF 協議。在這里我們介紹最為常見的一種:MESI數據一致性協議。

    在 MESI 協議中,每個緩存可能有有4個狀態,它們分別是:

    • M(Modified):這行數據有效,數據被修改了,和內存中的數據不一致,數據只存在于本 Cache 中。
    • E(Exclusive):這行數據有效,數據和內存中的數據一致,數據只存在于本 Cache 中。
    • S(Shared):這行數據有效,數據和內存中的數據一致,數據存在于很多 Cache 中。
    • I(Invalid):這行數據無效。

    那么在 MESI 協議的作用下,我們上面的線程執行過程就變為:

    • 線程 A 分配到 CPU0 執行,這時候讀取 i 的值為 0,存到 CPU0 的高速緩存中。
    • 線程 B 分配到 CPU1 執行,這時候讀取 i 的值為0,存到 CPU1 的高速緩存中。
    • CPU0 進行運算,得出結果 10,運算結束,寫回內存,此時內存 i 的值為 10。同時通過消息的方式告訴其他持有 i 變量的 CPU 緩存,將這個緩存的狀態值為 Invalid。
    • CPU1 進行運算,從 CPU 緩存取出值,但是發現這個緩存值被置為 Invalid了。于是重新去內存中讀取,讀取到 10 這個值放入 CPU 緩存。
    • CPU1 進行運算,得出結果 20,運算結束,寫回內存,此時內存 i 的值為 20。

    從上面的例子,我們可以知道 MESI 緩存一致性協議,本質上是定義了一些內存狀態,然后通過消息的方式通知其他 CPU 高速緩存,從而解決了數據一致性的問題。

    從操作系統說起

    操作系統,它屏蔽了底層硬件的操作細節,將各種硬件資源虛擬化,方便我們進行上層軟件的開發。在我們開發應用軟件的時候,我們不需要直接與硬件進行交互,只需要和操作系統交互即可。既然如此,那么操作系統就需要將硬件進行封裝,然后抽象出一些概念,方便上層應用使用。于是 CPU 時間片、內核態、用戶態等概念也誕生了。

    前面我們說到 CPU 與內存之間會存在緩存一致性問題,那操作系統抽象出來的 CPU 與內存也會面臨這樣的問題。因此,操作系統層面也需要去解決同樣的問題。所以,對于任何一個系統來說,它們都需要去解決這樣一個問題。我們把在特定的操作協議下,對特定內存或高速緩存進行讀寫訪問的過程進行抽象,得到的就是內存模型了。 無論是 Windows 系統,還是 Linux 系統,它們都有特定的內存模型。

    Java 語言是建立在操作系統上層的高級語言,它只能與操作系統進行交互,而不與硬件進行交互。與操作系統相對于硬件類似,操作系統需要抽象出內存模型,那么 Java 語言也需要抽象出相對于操作系統的內存模型。一般來說,編程語言也可以直接復用操作系統層面的內存模型,例如:C++ 語言就是這么做的。但由于不同操作系統的內存模型不同,有可能導致程序在一套平臺上并發完全正常,而在另外一套平臺上并發訪問卻經常出錯。因此在某些場景下,就必須針對不同的平臺來編寫程序。

    而我們都知道 Java 的最大特點是「Write Once, Run Anywhere」,即一次編譯哪里都可以運行。而為了達到這樣一個目標,Java 語言就必須在各個操作系統的基礎上進一步抽象,建立起一套對內存或高速緩存的讀寫訪問抽象標準。這樣就可以保證無論在哪個操作系統,只要遵循了這個規范,都能保證并發訪問是正常的。

    Java內存模型 - 不同層面抽象及方案

    Java 內存模型

    經過了前面的鋪墊,相信你已經明白了為什么要有 Java 內存模型,以及 Java 內存模型是什么,有了一個感性的理解。這里我們再給 Java 內存模型下一個較為準確的定義。

    Java 內存模型(Java Memory Model,JMM)用于屏蔽各種硬件和操作系統的內存訪問差異,以實現讓 Java 程序在各種平臺都能達到一致的內存訪問效果。

    Java 內存模型定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣的底層細節。這里說的變量包括了實例字段、靜態字段和構成數組對象的元素,但不包括局部變量與方法參數。因為后者是線程私有的,不會被共享,自然就不會存在競爭問題。

    內存模型的定義

    Java 內存模型規定所有的變量都存儲在主內存中,每條線程都有自己的工作內存。線程的工作內存中保存了被該線程使用到的變量的主內存副本拷貝,線程對變量的所有操作(讀取、賦值等)都必須在工作內存中進行,而不能直接讀寫主內存中的變量。不同線程之間也無法直接訪問對方工作內存中的變量,線程間變量值的傳遞都需要通過主內存來完成。主內存、工作內存、線程三者之間的關系如下圖所示。

    Java 內存模型圖解

    Java 內存模型的主內存、工作內存與 JVM 的堆、棧、方法區,并不是同一層次的內存劃分,兩者是沒有關聯的。如果一定要對應一下,那么主內存主要對應于 Java 堆中對象實例的數據部分,而工作內存則對應于虛擬機棧中的部分區域。

    內存間的交互

    關于主內存與工作內存之間具體的交互協議,即一個變量如何從主內存拷貝到工作內存,以及如何從工作內存同步回主內存的細節,Java 內存模型定義了 8 種操作來完成。虛擬機實現的時候必須保證下面提及的每一種操作都是原子的、不可再分的。

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

    如果要把一個變量從主內存復制到工作內存,那就要順序地執行 read 和 load 操作,如果要把變量從工作內存同步回主內存,就要順序地執行 store 和 write 操作。注意,Java 內存模型只要求上述兩個操作必須按順序執行,而沒有保證是連續執行。也就是說,read 與 load 之間、store 與 write 之間是可插入其他指令的,如對主內存中的變量 a、b 進行訪問時,一種可能出現順序是 read a、read b、load b、load a

    此外,Java 內存模型還規定上述 8 種基本操作時必須滿足如下規則:

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

    這 8 種內存訪問操作以及上述規則限定,再加上稍后介紹的對 volatile 的一些特殊規定,就已經完全確定了 Java 程序中哪些內存訪問操作在并發下是安全的。 看完了 Java 內存模型的 8 個基本操作和 8 個規則,感覺太過于繁瑣了,非常不利于我們日常代碼的編寫。為了能幫助編程人員理解,于是就有了與其相等價的判斷原則 —— 先行發生原則,它可以用于判斷一個訪問在并發環境下是否安全。

    總結

    這篇文章我們從底層 CPU 開始講起,一直講到操作系統,最后講到了編程語言層面,讓大家能夠一環扣一環地理解,最后明白 Java 內存模型誕生的原因(上層有數據一致性問題),以及最終要解決的問題(緩存一致性問題)。看到這里,我們大概把為什么要有 Java 內存模型講清楚了,也知道了 Java 內存模型是什么。最后我們來做個總結:

    1. 由于多核 CPU 和高速緩存在存在,導致了緩存一致性問題。這個問題屬于硬件層面上的問題,而解決辦法是各種緩存一致性協議。不同 CPU 采用的協議不同,MESI 是最經典的一個緩存一致性協議。
    2. 操作系統作為對底層硬件的抽象,自然也需要解決 CPU 高速緩存與內存之間的緩存一致性問題。各個操作系統都對 CPU 高速緩存與緩存的讀寫訪問過程進行抽象,最終得到的一個東西就是「內存模型」。
    3. Java 語言作為運行在操作系統層面的高級語言,為了解決多平臺運行的問題,在操作系統基礎上進一步抽象,得到了 Java 語言層面上的內存模型。
    4. Java 內存模型分為工作內存與主內存,每個線程都有自己的工作內存。每個線程都不能直接與主內存交互,只能與工作內存交互。此外,為了保證并發編程下的數據準確性,Java 內存模型還定義了 8 個基本的原子操作,以及 8 條基本的規則。

    如果 Java 程序能夠遵守 Java 內存模型的規則,那么其寫出的程序就是并發安全的,這就是 Java 內存模型最大的價值。

    深入理解 Java 內存模型

    參考資料

    posted @ 2022-06-22 10:39  陳樹義  閱讀(1084)  評論(3編輯  收藏  舉報
    国产美女a做受大片观看