Java內存模型(JMM)圖文並茂,條理清晰

什麼是Java內存模型(JMM)?

1. 爲什麼要有內存模型?

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

1.1. 硬件內存架構

在這裏插入圖片描述

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 三級緩存。

1.2. 緩存一致性問題

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

使用高速緩存解決了 CPU 和主存速率不匹配的問題,但同時又引入另外一個新問題:緩存一致性問題。
在這裏插入圖片描述

在多CPU的系統中(或者單CPU多核的系統),每個CPU內核都有自己的高速緩存,它們共享同一主內存(Main Memory)。當多個CPU的運算任務都涉及同一塊主內存區域時,CPU 會將數據讀取到緩存中進行運算,這可能會導致各自的緩存數據不一致。

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

1.3. 處理器優化和指令重排序

爲了提升性能在 CPU 和主內存之間增加了高速緩存,但在多線程併發場景可能會遇到緩存一致性問題。那還有沒有辦法進一步提升 CPU 的執行效率呢?答案是:處理器優化。

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

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

處理器優化其實也是重排序的一種類型,這裏總結一下,重排序可以分爲三種類型:

  • 編譯器優化的重排序。編譯器在不改變單線程程序語義放入前提下,可以重新安排語句的執行順序。
  • 指令級並行的重排序。現代處理器採用了指令級並行技術來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。
  • 內存系統的重排序。由於處理器使用緩存和讀寫緩衝區,這使得加載和存儲操作看上去可能是在亂序執行。

2. 併發編程的問題

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

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

在這裏插入圖片描述

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

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

所以技術前輩們想到了在物理機器上定義出一套內存模型, 規範內存的讀寫操作。內存模型解決併發問題主要採用兩種方式:限制處理器優化和使用內存屏障。

3. Java 內存模型

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

3.1. Java 運行時內存區域與硬件內存的關係

瞭解過 JVM 的同學都知道,JVM 運行時內存區域是分片的,分爲棧、堆等,其實這些都是 JVM 定義的邏輯概念。在傳統的硬件內存架構中是沒有棧和堆這種概念。
在這裏插入圖片描述

從圖中可以看出棧和堆既存在於高速緩存中又存在於主內存中,所以兩者並沒有很直接的關係。

3.2. Java 線程與主內存的關係

Java 內存模型是一種規範,定義了很多東西:

  • 所有的變量都存儲在主內存(Main Memory)中。
  • 每個線程都有一個私有的本地內存(Local Memory),本地內存中存儲了該線程以讀/寫共享變量的拷貝副本。
  • 線程對變量的所有操作都必須在本地內存中進行,而不能直接讀寫主內存。
  • 不同的線程之間無法直接訪問對方本地內存中的變量。

看文字太枯燥了,我又畫了一張圖:
在這裏插入圖片描述

3.3. 線程間通信

如果兩個線程都對一個共享變量進行操作,共享變量初始值爲 1,每個線程都變量進行加 1,預期共享變量的值爲 3。在 JMM 規範下會有一系列的操作。
在這裏插入圖片描述

爲了更好的控制主內存和本地內存的交互,Java 內存模型定義了八種操作來實現:

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

注意:工作內存也就是本地內存的意思。

4. 有態度的總結

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

數據同時存在於高速緩存和主內存中,如果不加以規範勢必造成災難,因此在傳統機器上又抽象出了內存模型。

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

爲了更精準控制工作內存和主內存間的交互,JMM 還定義了八種操作:lock, unlock, read, load,use,assign, store, write。

– End –

Java內存模型(JMM)圖文並茂,條理清晰!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章