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