Java虛擬機規(guī)范中定義了Java內存模型(Java Memory Model,JMM),用于屏蔽掉各種硬件和操作系統(tǒng)的內存訪問差異,以實現讓Java程序在各種平臺下都能達到一致的并發(fā)效果。
為什么要有內存模型?
要想回答這個問題,我們需要先弄懂傳統(tǒng)計算機硬件內存架構。
1.1 硬件內存架構
(1)CPU
一個現代計算機通常由兩個或者多個CPU。其中一些CPU還有多核。從這一點可以看出,在一個有兩個或者多個CPU的現代計算機上同時運行多個線程是可能的。每個CPU在某一時刻運行一個線程是沒有問題的。這意味著,如果你的Java程序是多線程的,在你的Java程序中每個CPU上一個線程可能同時(并發(fā))執(zhí)行。
(2)CPU寄存器
每個CPU都包含一系列的寄存器,它們是CPU內內存的基礎。CPU在寄存器上執(zhí)行操作的速度遠大于在主存上執(zhí)行的速度。這是因為CPU訪問寄存器的速度遠大于主存。
(3)CPU 高速緩存
由于計算機的存儲設備與處理器的運算速度之間有著幾個數量級的差距,所以現代計算機系統(tǒng)都不得不加入一層讀寫速度盡可能接近處理器運算速度的高速緩存來作為內存與處理器之間的緩沖:將運算需要使用到的數據復制到緩存中,讓運算能快速進行,當運算結束后再從緩存同步回內存之中,這樣處理器就無須等待緩慢的內存讀寫了。CPU訪問緩存層的速度快于訪問主存的速度,但通常比訪問內部寄存器的速度還要慢一點。每個CPU可能有一個CPU緩存層,一些CPU還有多層緩存。在某一時刻,一個或者多個緩存行(cache lines)可能被讀到緩存,一個或者多個緩存行可能再被刷新回主存。
(4)主存
主存比 L1、L2 緩存要大很多。
注意:部分高端機器還有 L3 三級緩存。
1.2 緩存一致性問題
多處理器系統(tǒng)中,每個處理器都有自己的高速緩存,而它們又共享同一主內存(MainMemory)?;诟咚倬彺娴拇鎯换ズ芎玫亟鉀Q了處理器與內存的速度矛盾,但是也引入了新的問題:緩存一致性(CacheCoherence)。
當多個處理器的運算任務都涉及同一塊主內存區(qū)域時,將可能導致各自的緩存數據不一致的情況,如果真的發(fā)生這種情況,那同步回到主內存時以誰的緩存數據為準呢?
為了解決一致性的問題,需要各個處理器訪問緩存時都遵循一些協(xié)議,在讀寫時要根據協(xié)議來進行操作,這類協(xié)議有MSI、MESI(IllinoisProtocol)、MOSI、Synapse、Firefly及DragonProtocol等等。
1.3 處理器優(yōu)化和指令重排序
為了提升性能在 CPU 和主內存之間增加了高速緩存,但在多線程并發(fā)場景可能會遇到緩存一致性問題。那還有沒有辦法進一步提升 CPU 的執(zhí)行效率呢?答案是:處理器優(yōu)化。
為了使處理器內部的運算單元能夠最大化被充分利用,處理器會對輸入代碼進行亂序執(zhí)行處理,這就是處理器優(yōu)化。
除了處理器會對代碼進行優(yōu)化處理,很多現代編程語言的編譯器也會做類似的優(yōu)化,比如像 Java 的即時編譯器(JIT)會做指令重排序。
為了使得處理器內部的運算單元能盡量被充分利用,處理器可能會對輸入代碼進行亂序執(zhí)行(Out-Of-Order Execution)優(yōu)化,處理器會在計算之后將亂序執(zhí)行的結果重組,保證該結果與順序執(zhí)行的結果是一致的,但并不保證程序中各個語句計算的先后順序與輸入代碼中的順序一致。
因此,如果存在一個計算任務依賴另一個計算任務的中間結果,那么其順序性并不能靠代碼的先后順序來保證。與處理器的亂序執(zhí)行優(yōu)化類似,Java虛擬機的即時編譯器中也有類似的指令重排序(Instruction Reorder)優(yōu)化。
重排序可以分為三種類型:
編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語義放入前提下,可以重新安排語句的執(zhí)行順序。
指令級并行的重排序?,F代處理器采用了指令級并行技術來將多條指令重疊執(zhí)行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執(zhí)行順序。
內存系統(tǒng)的重排序。由于處理器使用緩存和讀寫緩沖區(qū),這使得加載和存儲操作看上去可能是在亂序執(zhí)行。
02
并發(fā)編程的問題
并發(fā)的三個問題:『可見性問題』、『原子性問題』、『有序性問題』。如果從更深層次看這三個問題,其實就是上面講的『緩存一致性』、『處理器優(yōu)化』、『指令重排序』造成的。
緩存一致性問題其實就是可見性問題,處理器優(yōu)化可能會造成原子性問題,指令重排序會造成有序性問題,你看是不是都聯(lián)系上了。
出了問題總是要解決的,那有什么辦法呢?首先想到簡單粗暴的辦法,干掉緩存讓 CPU 直接與主內存交互就解決了可見性問題,禁止處理器優(yōu)化和指令重排序就解決了原子性和有序性問題,但這樣一夜回到解放前了,顯然不可取。
所以技術前輩們想到了在物理機器上定義出一套內存模型, 規(guī)范內存的讀寫操作。內存模型解決并發(fā)問題主要采用兩種方式:限制處理器優(yōu)化和使用內存屏障。
03
Java 內存模型
同一套內存模型規(guī)范,不同語言在實現上可能會有些差別。接下來著重講一下 Java 內存模型實現原理。
3.1 Java運行時內存區(qū)域與硬件內存的關系
Java內存模型與硬件內存架構之間存在差異。硬件內存架構沒有區(qū)分線程棧和堆。對于硬件,所有的線程棧和堆都分布在主內存中。部分線程棧和堆可能有時候會出現在CPU緩存中和CPU內部的寄存器中。如下圖所示:
3.2 Java線程與主內存的關系
從抽象的角度來看,JMM定義了線程和主內存之間的抽象關系:
線程之間的共享變量存儲在主內存(Main Memory)中
每個線程都有一個私有的本地內存(Local Memory),本地內存是JMM的一個抽象概念,并不真實存在,它涵蓋了緩存、寫緩沖區(qū)、寄存器以及其他的硬件和編譯器優(yōu)化。本地內存中存儲了該線程以讀/寫共享變量的拷貝副本。
從更低的層次來說,主內存就是硬件的內存,而為了獲取更好的運行速度,虛擬機及硬件系統(tǒng)可能會讓工作內存優(yōu)先存儲于寄存器和高速緩存中。
Java內存模型中的線程的工作內存(working memory)是cpu的寄存器和高速緩存的抽象描述。而JVM的靜態(tài)內存儲模型(JVM內存模)只是一種對內存的物理劃分而已,它只局限在內存,而且只局限在JVM的內存。
線程間通信
線程間通信必須要經過主內存。
如下,如果線程1與線程2之間要通信的話,必須要經歷下面2個步驟:
1)線程1把本地內存A中更新過的共享變量刷新到主內存中去。
2)線程2到主內存中去讀取線程A之前已更新過的共享變量。
關于主內存與工作內存之間的具體交互協(xié)議,即一個變量如何從主內存拷貝到工作內存、如何從工作內存同步到主內存之間的實現細節(jié),Java內存模型定義了以下八種操作來完成:
lock(鎖定):作用于主內存的變量,把一個變量標識為一條線程獨占狀態(tài)。
unlock(解鎖):作用于主內存變量,把一個處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可以被其他線程鎖定。
read(讀取):作用于主內存變量,把一個變量值從主內存?zhèn)鬏數骄€程的工作內存中,以便隨后的load動作使用
load(載入):作用于工作內存的變量,它把read操作從主內存中得到的變量值放入工作內存的變量副本中。
use(使用):作用于工作內存的變量,把工作內存中的一個變量值傳遞給執(zhí)行引擎,每當虛擬機遇到一個需要使用變量的值的字節(jié)碼指令時將會執(zhí)行這個操作。
assign(賦值):作用于工作內存的變量,它把一個從執(zhí)行引擎接收到的值賦值給工作內存的變量,每當虛擬機遇到一個給變量賦值的字節(jié)碼指令時執(zhí)行這個操作。
store(存儲):作用于工作內存的變量,把工作內存中的一個變量的值傳送到主內存中,以便隨后的write的操作。
write(寫入):作用于主內存的變量,它把store操作從工作內存中一個變量的值傳送到主內存的變量中。
注意:工作內存也就是本地內存的意思。
04
總結
由于CPU 和主內存間存在數量級的速率差,想到了引入了多級高速緩存的傳統(tǒng)硬件內存架構來解決,多級高速緩存作為 CPU 和主內間的緩沖提升了整體性能。解決了速率差的問題,卻又帶來了緩存一致性問題。
數據同時存在于高速緩存和主內存中,如果不加以規(guī)范勢必造成災難,因此在傳統(tǒng)機器上又抽象出了內存模型。
Java 語言在遵循內存模型的基礎上推出了 JMM 規(guī)范,目的是解決由于多線程通過共享內存進行通信時,存在的本地內存數據不一致、編譯器會對代碼指令重排序、處理器會對代碼亂序執(zhí)行等帶來的問題。
為了更精準控制工作內存和主內存間的交互,JMM 還定義了八種操作:lock, unlock, read, load,use,assign, store, write。
(責任編輯:代碼如詩) |