學Java多線程必知的Java內存模型JMM

學Java多線程必知的Java內存模型JMM

JMM 即 Java Memory Model,它定義了主存、工作內存抽象概念,底層對應着 CPU 寄存器、緩存、硬件內存、

JMM 體現在以下幾個方面

  • 原子性 - 保證指令不會受到線程上下文切換的影響
  • 可見性 - 保證指令不會受 cpu 緩存的影響
  • 有序性 - 保證指令不會受 cpu 指令並行優化的影響

我們熟知的valatile關鍵字就是確保了可見性與有序性,但注意,valatile不確保原子性

JVM與JMM

首先,不要把JVM內存模型與JMM內存模型搞混淆了

我們常說的JVM內存模式指的是JVM的內存分區,而Java內存模式是一種虛擬機規範

Java虛擬機規範中定義了Java內存模型(Java Memory Model,JMM),用於屏蔽掉各種硬件和操作系統的內存訪問差異,以實現讓Java程序在各種平臺下都能達到一致的併發效果

JMM規範了Java虛擬機與計算機內存是如何協同工作的:

一個線程如何與何時可以看到由其他線程修改過後的共享變量的值,以及在必須時如何同步的訪問共享變量

Java內存模型跟CPU緩存模型類似,是基於CPU緩存模型來建立的,Java內存模型是標準化的,屏蔽掉了底層不同計算機的區別,在任何計算機上,JMM都可以抽象成線程與內存

首先我們得看看Java內存模型是基於底層怎樣的機制建立起來的

CPU多級緩存結構

  • 多CPU:現代計算機一般爲多核或多CPU,這也是多線程的基石
  • CPU寄存器(CPU Registers):每個CPU都包含一系列的寄存器,它們是CPU內內存的基礎。CPU在寄存器上執行操作的速度遠大於在主存上執行的速度。這是因爲CPU訪問寄存器的速度遠大於主存
  • 高速緩存cache:由於計算機的存儲設備與處理器的運算速度之間有着幾個數量級的差距,所以現代計算機系統都不得不加入一層讀寫速度儘可能接近處理器運算速度的高速緩存(Cache)來作爲內存與處理器之間的緩衝:將運算需要使用到的數據複製到緩存中,讓運算能快速進行,當運算結束後再從緩存同步回內存之中,這樣處理器就無須等待緩慢的內存讀寫了。CPU訪問緩存層的速度快於訪問主存的速度,但通常比訪問內部寄存器的速度還要慢一點。每個CPU可能有一個CPU緩存層,一些CPU還有多層緩存。在某一時刻,一個或者多個緩存行(cache lines)可能被讀到緩存,一個或者多個緩存行可能再被刷新回主存
  • 內存:一個計算機還包含一個主存。所有的CPU都可以訪問主存。主存通常比CPU中的緩存大得多。
  • 運作原理:通常情況下,當一個CPU需要讀取主存時,它會將主存的部分讀到CPU緩存中。它甚至可能將緩存中的部分內容讀到它的內部寄存器中,然後在寄存器中執行操作。當CPU需要將結果寫回到主存中去時,它會將內部寄存器的值刷新到緩存中,然後在某個時間點將值刷新回主存

如圖,JMM定義了主存、本地內存的抽象概念,底層實際上對應着 CPU 寄存器、緩存、硬件內存、CPU 指令優化等

  • 線程之間的共享變量存儲在主內存(Main Memory)中

  • 每個線程都有一個私有的本地內存(Local Memory),本地內存是JMM的一個抽象概念,並不真實存在,它涵蓋了CPU緩存、寫緩衝區、CPU寄存器以及其他的硬件和編譯器優化。本地內存中存儲了該共享變量的拷貝副本

  • Java內存模型中的線程的工作內存(working memory)是cpu的寄存器和高速緩存的抽象描述。而JVM的靜態內存儲模型(JVM內存模型)只是一種對內存的物理劃分而已,它只侷限在內存,而且只侷限在JVM的內存

緩存一致性問題

在多處理器系統中,每個處理器都有自己的高速緩存,而它們又共享同一主內存(MainMemory)

基於高速緩存的存儲交互很好地解決了處理器與內存的速度矛盾,但是也引入了新的問題:緩存一致性(CacheCoherence)

當多個處理器的運算任務都涉及同一塊主內存區域時,將可能導致各自的緩存數據不一致的情況,如果真的發生這種情況,那同步回到主內存時以誰的緩存數據爲準呢?

爲了解決一致性的問題,需要各個處理器訪問緩存時都遵循一些協議,在讀寫時要根據協議來進行操作,這類協議有MSI、MESI(IllinoisProtocol)、MOSI、Synapse、Firefly及DragonProtocol,等等

指令重排序問題

爲了使得處理器內部的運算單元能儘量被充分利用,處理器可能會對輸入代碼進行亂序執行(Out-Of-Order Execution)優化

處理器會在計算之後將亂序執行的結果重組,保證該結果與順序執行的結果是一致的,但並不保證程序中各個語句計算的先後順序與輸入代碼中的順序一致。

因此,如果存在一個計算任務依賴另一個計算任務的中間結果,那麼其順序性並不能靠代碼的先後順序來保證

與處理器的亂序執行優化類似,Java虛擬機的即時編譯器中也有類似的指令重排序(Instruction Reorder)優化

多線程下指令重排會影響運行結果的正確性,適當使用valatile禁用指令重排是必要的,當然這裏說的禁用並不是簡單的不讓指令重排序,而是加入讀寫屏障保證valatile的有序性,這點我們在說valatile的時候會講到

JVM和硬件內存架構

Java內存模型與硬件內存架構之間存在差異。硬件內存架構沒有區分線程棧和堆。對於硬件,所有的線程棧和堆都分佈在主內存中

部分線程棧和堆可能有時候會出現在CPU緩存中和CPU內部的寄存器中

如下圖所示:

JMM下線程間的通信

關於主內存與工作內存(本地內存)之間的具體交互協議,即一個變量如何從主內存拷貝到工作內存、如何從工作內存同步到主內存之間的實現細節,Java內存模型定義了以下八種原子操作來完成:

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

對應在JMM中,示例圖

JMM多線程下解決緩存不一致問題

JMM依賴於底層CPU的解決緩存不一致問題

  • 總線加鎖(低性能)

    CPU從主內存讀取數據到高速緩存,會在總線對這個數據加鎖,這樣其他CPU沒法去讀或寫這個數據,直到這個CPU使用完數據釋放鎖之後其他CPU才能讀取該數據

  • MESI緩存一致協議

    多個CPU(核)從主內存讀取同一個數據到各自的高速緩存,當其中某個CPU修改了緩存裏的數據,該數據會馬上同步會主內存,其他CPU通過總線嗅探機制可以感知主線程中數據的變化從而將自己緩存(工作內存)中的數據失效掉

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