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