首頁 > 軟體

全面學習掌握Java記憶體模型

2021-05-24 11:30:34

我們常說的JVM記憶體模式指的是JVM的記憶體分區;而Java記憶體模式是一種虛擬機器規範。

Java虛擬機器規範中定義了Java記憶體模型(Java Memory Model,JMM),用於遮蔽掉各種硬體和作業系統的記憶體訪問差異,以實現讓Java程式在各種平臺下都能達到一致的併發效果,JMM規範了Java虛擬機器與計算機記憶體是如何協同工作的:規定了一個執行緒如何和何時可以看到由其他執行緒修改過後的共享變數的值,以及在必須時如何同步的訪問共享變數。

原始的Java記憶體模型存在一些不足,因此Java記憶體模型在Java1.5時被重新修訂。這個版本的Java記憶體模型在Java8中仍然在使用。

在面試中,面試官經常喜歡問:『說說什麼是Java記憶體模型(JMM)?』

面試者內心狂喜,這題剛背過:『Java記憶體主要分為五大塊:堆、方法區、虛擬機器棧、本地方法棧、PC寄存器,balabala……』

面試官會心一笑,露出一道光芒:『好了,今天的面試先到這裡了,回去等通知吧』

一般聽到等通知這句話,這場面試大概率就是涼涼了。為什麼呢?因為面試者弄錯了概念,面試官是想考察JMM,但是面試者一聽到

Java記憶體

這幾個關鍵字就開始背誦八股文了。Java記憶體模型(JMM)和 Java 運行時記憶體區域區別可大了呢,不要走開接著往下看,答應我要看完。

為什麼要有記憶體模型?

要想回答這個問題,我們需要先弄懂傳統計算機硬體記憶體架構。好了,我要開始畫圖了。

硬體記憶體架構

(1)CPU

去過機房的同學都知道,一般在大型伺服器上會配置多個CPU,每個CPU還會有多個

,這就意味著多個CPU或者多個核可以同時(併發)工作。如果使用Java 起了一個多執行緒的任務,很有可能每個 CPU 都會跑一個執行緒,那麼你的任務在某一刻就是真正併發執行了。

(2)CPU Register

CPU Register也就是 CPU 寄存器。CPU 寄存器是 CPU 內部整合的,在寄存器上執行操作的效率要比在主存上高出幾個數量級。

(3)CPU Cache Memory

CPU Cache Memory也就是 CPU 快取記憶體,相對於寄存器來說,通常也可以成為 L2 二級快取。相對於硬碟讀取速度來說記憶體讀取的效率非常高,但是與 CPU 還是相差數量級,所以在 CPU 和主存間引入了多級快取,目的是為了做一下緩衝。

(4)Main Memory

Main Memory 就是主存,主存比 L1、L2 快取要大很多。

注意:部分高階機器還有 L3 三級快取。

快取一致性問題

由於主存與 CPU 處理器的運算能力之間有數量級的差距,所以在傳統計算機記憶體架構中會引入快取記憶體來作為主存和處理器之間的緩衝,CPU 將常用的資料放在快取記憶體中,運算結束後 CPU 再講運算結果同步到主存中。

使用快取記憶體解決了 CPU 和主存速率不匹配的問題,但同時又引入另外一個新問題:快取一致性問題。

在多CPU的系統中(或者單CPU多核的系統),每個CPU核心都有自己的快取記憶體,它們共享同一主記憶體(Main Memory)。當多個CPU的運算任務都涉及同一塊主記憶體區域時,CPU 會將資料讀取到快取中進行運算,這可能會導致各自的快取資料不一致。

因此需要每個 CPU 訪問快取時遵循一定的協議,在讀寫資料時根據協議進行操作,共同來維護快取的一致性。這類協議有 MSI、MESI、MOSI、和 Dragon Protocol 等。

處理器優化和指令重排序

為了提升效能在 CPU 和主記憶體之間增加了快取記憶體,但在多執行緒併發場景可能會遇到

。那還有沒有辦法進一步提升 CPU 的執行效率呢?答案是:處理器優化。

為了使處理器內部的運算單元能夠最大化被充分利用,處理器會對輸入程式碼進行亂序執行處理,這就是處理器優化。

除了處理器會對程式碼進行優化處理,很多現代程式語言的編譯器也會做類似的優化,比如像 Java 的即時編譯器(JIT)會做指令重排序。

處理器優化其實也是重排序的一種類型,這裡總結一下,重排序可以分為三種類型:編譯器優化的重排序。編譯器在不改變單執行緒程式語義放入前提下,可以重新安排語句的執行順序。指令級並行的重排序。現代處理器採用了指令級並行技術來將多條指令重疊執行。如果不存在資料依賴性,處理器可以改變語句對應機器指令的執行順序。記憶體系統的重排序。由於處理器使用快取和讀寫緩衝區,這使得載入和儲存操作看上去可能是在亂序執行。

併發程式設計的問題

上面講了一堆硬體相關的東西,有些同學可能會有點懵,繞了這麼大圈,這些東西跟 Java 記憶體模型有啥關係嗎?不要急咱們慢慢往下看。

熟悉 Java 併發的同學肯定對這三個問題很熟悉:『可見性問題』、『原子性問題』、『有序性問題』。如果從更深層次看這三個問題,其實就是上面講的『快取一致性』、『處理器優化』、『指令重排序』造成的。

快取一致性問題其實就是可見性問題,處理器優化可能會造成原子性問題,指令重排序會造成有序性問題,你看是不是都聯絡上了。

出了問題總是要解決的,那有什麼辦法呢?首先想到簡單粗暴的辦法,幹掉快取讓 CPU 直接與主記憶體互動就解決了可見性問題,禁止處理器優化和指令重排序就解決了原子性和有序性問題,但這樣一夜回到解放前了,顯然不可取。

所以技術前輩們想到了在物理機器上定義出一套記憶體模型, 規範記憶體的讀寫操作。記憶體模型解決併發問題主要採用兩種方式:

限制處理器優化和使用記憶體屏障。

Java 記憶體模型

同一套記憶體模型規範,不同語言在實現上可能會有些差別。接下來著重講一下 Java 記憶體模型實現原理。

Java 運行時記憶體區域與硬體記憶體的關係

瞭解過 JVM 的同學都知道,JVM 運行時記憶體區域是分片的,分為棧、堆等,其實這些都是 JVM 定義的邏輯概念。在傳統的硬體記憶體架構中是沒有棧和堆這種概念。

從圖中可以看出棧和堆既存在於快取記憶體中又存在於主記憶體中,所以兩者並沒有很直接的關係。

Java 執行緒與主記憶體的關係

Java 記憶體模型是一種規範,定義了很多東西:

所有的變數都儲存在主記憶體(Main Memory)中。每個執行緒都有一個私有的本地記憶體(Local Memory),本地記憶體中儲存了該執行緒以讀/寫共享變數的拷貝副本。執行緒對變數的所有操作都必須在本地記憶體中進行,而不能直接讀寫主記憶體。不同的執行緒之間無法直接訪問對方本地記憶體中的變數。看文字太枯燥了,我又畫了一張圖:

執行緒間通訊

如果兩個執行緒都對一個共享變數進行操作,共享變數初始值為 1,每個執行緒都變數進行加 1,預期共享變數的值為 3。在 JMM 規範下會有一系列的操作。

為了更好的控制主記憶體和本地記憶體的互動,Java 記憶體模型定義了八種操作來實現:

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

有態度的總結

由於CPU 和主記憶體間存在數量級的速率差,想到了引入了多級快取記憶體的傳統硬體記憶體架構來解決,多級快取記憶體作為 CPU 和主內間的緩衝提升了整體效能。解決了速率差的問題,卻又帶來了快取一致性問題。

資料同時存在於快取記憶體和主記憶體中,如果不加以規範勢必造成災難,因此在傳統機器上又抽象出了記憶體模型。

Java 語言在遵循記憶體模型的基礎上推出了 JMM 規範,目的是解決由於多執行緒通過共享記憶體進行通訊時,存在的本地記憶體資料不一致、編譯器會對程式碼指令重排序、處理器會對程式碼亂序執行等帶來的問題。

為了更精準控制工作記憶體和主記憶體間的互動,JMM 還定義了八種操作:lock, unlock, read, load,use,assign, store, write。

好了,今天就給大家介紹到這裡,簡單總結下Java記憶體模型的定義:Java記憶體模型並不是一件容易的事情,這個模型必須定義得足夠嚴謹,才能讓Java的併發操作不會產生歧義;但是,也必須得足夠寬鬆,使得虛擬機器的實現能有足夠的自由空間去利用硬體的各種特性(寄存器、快取記憶體等)來獲取更好的執行速度。經過長時間的驗證和修補,在JDK1.5釋出後,Java記憶體模型就已經成熟和完善起來了。

希望大家看完本文有所收穫,謝謝大家支援


IT145.com E-mail:sddin#qq.com