從 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。下面是可能發生的一種情況:

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

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

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

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

加入緩存一致性協議

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

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

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

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

從上面的例子,我們可以知道 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 種操作來完成。虛擬機實現的時候必須保證下面提及的每一種操作都是原子的、不可再分的。

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

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

這 8 種內存訪問操作以及上述規則限定,再加上稍後介紹的對 volatile 的一些特殊規定,就已經完全確定了 Java 程序中哪些內存訪問操作在併發下是安全的。 

總結

這篇文章我們從底層 CPU 開始講起,一直講到操作系統,最後講到了編程語言層面,讓大家能夠一環扣一環地理解,最後明白 Java 內存模型誕生的原因(上層有數據一致性問題),以及最終要解決的問題(緩存一致性問題)。

看到這裏,我們大概把爲什麼要有 Java 內存模型講清楚了,也知道了 Java 內存模型是什麼。最後我們來做個總結:

  1. 由於多核 CPU 和高速緩存在存在,導致了緩存一致性問題。這個問題屬於硬件層面上的問題,而解決辦法是各種緩存一致性協議。不同 CPU 採用的協議不同,MESI 是最經典的一個緩存一致性協議。

  2. 操作系統作爲對底層硬件的抽象,自然也需要解決 CPU 高速緩存與內存之間的緩存一致性問題。各個操作系統都對 CPU 高速緩存與緩存的讀寫訪問過程進行抽象,最終得到的一個東西就是「內存模型」。

  3. Java 語言作爲運行在操作系統層面的高級語言,爲了解決多平臺運行的問題,在操作系統基礎上進一步抽象,得到了 Java 語言層面上的內存模型。

  4. Java 內存模型分爲工作內存與主內存,每個線程都有自己的工作內存。每個線程都不能直接與主內存交互,只能與工作內存交互。此外,爲了保證併發編程下的數據準確性,Java 內存模型還定義了 8 個基本的原子操作,以及 8 條基本的規則。

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

深入理解 Java 內存模型

參考資料

陳樹義 八年研發老兵,搞過架構,帶過團隊,曾就職於唯品會、SHEIN 等電商公司。擅長用簡潔有趣的語言講解技術,所寫 JVM 入門教程全網超 30 萬閱讀,受到廣大網友一致好評。關注我,獲取更多有趣又硬核的 Java 技術知識!

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/UDets3zqMz2IwzODA2Lqqw