Java內(nèi)存模型(JMM)詳解

預(yù)警

  • 本文約4千字,預(yù)計花費15分鐘閱讀完~~
  • 本文內(nèi)容比較枯燥,但如果你能認(rèn)認(rèn)真真的看完,那你對Java內(nèi)存模型將會有更深入的了解~~~

概述

多任務(wù)處理是現(xiàn)代計算機操作系統(tǒng)中必備的一項技能,在許多情況下,讓計算機同時去處理幾件事,不僅是因為其運算能力強大,還有一個很重要的原因是計算機的運算速度與它的存儲和通訊子系統(tǒng)速度的差距太大,其實大部分時間都花在了磁盤IO、網(wǎng)絡(luò)通訊和數(shù)據(jù)訪問上。
如果我們不希望處理器大部分時間都在等待其他資源時,就必須使用一些手段去把處理器的運算能力壓榨出來,否則就造成了很大的浪費,那么讓計算機同時處理幾項任務(wù)就是最容易想到的。
讓計算機并發(fā)執(zhí)行若干個運算任務(wù)更充分地利用計算機處理器的效能之間的因果關(guān)系,從上面的論調(diào)上看起來順理成章,但實際上并沒有想象中的那么容易實現(xiàn),因為所有的運算任務(wù)都不可能只靠處理器計算就能完成,至少與內(nèi)存的交互,如讀取運算數(shù)據(jù)、存儲運算結(jié)果等,就是很難消除的。
由于計算機的存儲設(shè)備與處理器的運算速度之間有著幾個數(shù)量級的差距,所以現(xiàn)代計算機系統(tǒng)都加入了一層讀寫速度盡可能接近處理器運算速度的高速緩存(Cache)來作為內(nèi)存與處理器之間的緩存:將運算需要使用到的數(shù)據(jù)復(fù)制到緩存中,讓運算能快速進(jìn)行,當(dāng)運算結(jié)束后再從緩存同步回內(nèi)存之中,就樣處理器就無須等待緩慢的內(nèi)存讀寫了。
基于高速緩存的存儲交互很好地解決了處理器與內(nèi)存的速度矛盾,但是也引入了新的問題:緩存一致性(Cache Coherence)。在多處理器系統(tǒng)中,每個處理器都有自己的高速緩存,面它們又共享同一主內(nèi)存(Main Memory)。如下圖:

處理器、高速緩存、主內(nèi)存間的交互關(guān)系

當(dāng)多個處理器的運算任務(wù)都涉及同一塊主內(nèi)存區(qū)域時,將可能導(dǎo)致各自的緩存數(shù)據(jù)不致的情況,如果真的發(fā)生這種情況,那同步回到主內(nèi)存時以誰的緩存數(shù)據(jù)為準(zhǔn)?為了解決一致性的問題,需要各個處理器訪問緩存時都遵循一些協(xié)議,在讀寫時要根據(jù)協(xié)議來進(jìn)行操作,這類協(xié)議有MSI,MESI(Illinois Protocol),MOSI,Synapse,FireflyDragon Protocol等等。
Java虛擬機內(nèi)存模型中定義的內(nèi)存訪問操作與硬件的緩存訪問操作是具有可比性的。

除此之外,為了使得處理器內(nèi)部的運算單元盡量補充分利用,處理器可能會對輸入代碼進(jìn)行亂序執(zhí)行(Out-Of-Order Execution)優(yōu)化,處理器會在計算之后將亂序執(zhí)行的結(jié)果重組,保證該結(jié)果與順序執(zhí)行的結(jié)果一致的,但并不保證程序中各個語句計算的先后順序與輸入代碼中的順序一致,因此如果存在一個計算任務(wù)依賴另一個計算任務(wù)的中間結(jié)果,那么其順序性并不能靠代碼的先后順序來保證。與處理器的亂序執(zhí)行優(yōu)化類似的,Java虛擬機的即時編譯器中也有類似的指令重排序(Instruction Reorder)優(yōu)化

Java內(nèi)存模型

Java虛擬機規(guī)范中試圖定義一種Java內(nèi)存模型(Java Memory Model,JMM)來屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,以實現(xiàn)讓Java程序在各種平臺下都能達(dá)到一致的并發(fā)效果。
在此之前,主流程序語言(如C/C++等)直接使用物理硬件,因此,會由于不同平臺上內(nèi)存模型的差異,導(dǎo)致程序在一套平臺上并發(fā)完全正常,而在另一套平臺上并發(fā)訪問卻經(jīng)常出錯,因此經(jīng)常需要針對不同的平臺來編寫程序。
定義Java內(nèi)存模型并非一件容易的事情,這個模型必須定義得足夠嚴(yán)謹(jǐn),才能讓Java的并發(fā)操作不會產(chǎn)生歧義;但是,也必須定義得足夠?qū)捤桑沟锰摂M機的實現(xiàn)能有足夠的自由空間去利用硬件的各種特性(寄存器、高速緩存等)來獲取更好的執(zhí)行速度。
經(jīng)過長時間的驗證和修補,在JDK1.5之后,Java的內(nèi)存模型就已經(jīng)成熟和完善起來了。

主內(nèi)存與工作內(nèi)存

Java內(nèi)存模型的主要目標(biāo)是定義程序中各個變量的訪問規(guī)則,即在虛擬機中將變量存儲到內(nèi)存和從內(nèi)存中取出變量這樣的底層細(xì)節(jié)。 這里的變量(Variable)與Java編程中所說的變量略有區(qū)別,這里說的變量包含了實例字段、靜態(tài)字段和構(gòu)成數(shù)組對象的元素,但不包括局部變量與方法參數(shù),因為局部變量與方法參數(shù)是線程私有的,不會被共享,自然就不會存在競爭問題。
為了獲得較好的執(zhí)行效能,Java內(nèi)存模型并沒有限制執(zhí)行引擎使用處理器的特定寄存器或緩存來和主內(nèi)存進(jìn)行交互也沒有限制即時編譯器調(diào)整代碼執(zhí)行順序這類的權(quán)利。
Java內(nèi)存模型規(guī)定了所有變量都存儲在主內(nèi)存(Main Memory)中(這里的主內(nèi)存僅是虛擬機內(nèi)存的一部分)。每條線程還有自己的工作內(nèi)存(Working Memory,可與前面所講的處理器高速緩存類比),線程的工作內(nèi)存中保存了使用變量的主內(nèi)存副本,線程對變量的操作(讀取、賦值等)都必須在工作內(nèi)存中進(jìn)行,不能直接讀寫主內(nèi)存中的變量。
不同上的線程之間也無法直接訪問對方工作內(nèi)存中的變量,線程間變量值的傳遞均需要通過主內(nèi)存來完成,線程、主內(nèi)存、工作內(nèi)存三者的交互關(guān)系如下圖:

線程、主內(nèi)存、工作內(nèi)存三者的交互關(guān)系

我們可以看到,Java內(nèi)存模型與硬件的內(nèi)存模型是具有很大的相似度的。

內(nèi)存間交互操作

關(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)識為一條線程獨占的狀態(tài)。
  • unlock(解鎖) - 作用于主內(nèi)存的變量,它把一個處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可能被其他線程鎖定。
  • read(讀?。?/strong> - 作用于主內(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)虛擬機遇到一個需要使用到變量的值的字節(jié)碼指令時將會執(zhí)行這個操作。
  • assign(賦值) - 作用于工作內(nèi)存的變量,它把一個從執(zhí)行引擎接收到的值賦值給工作內(nèi)存的變量,每當(dāng)虛擬朵遇到一個給變量賦值的字節(jié)碼指令時執(zhí)行這個操作。
  • store(存儲) - 作用于工作內(nèi)存的變量,它把工作內(nèi)存中一個變量的值傳送到主內(nèi)存中,以便隨后的write操作使用。
  • write(寫入) - 作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中。

如果要把一個變量從主內(nèi)存復(fù)制到工作內(nèi)存,那就要按順序地執(zhí)行read和load操作,如果要把變量從工作內(nèi)存同步回主內(nèi)存,就要按順序執(zhí)行store和write操作。

注意:Java內(nèi)存模型只要求上述兩個操作必須按順序執(zhí)行,而沒有保證必須是連續(xù)執(zhí)行的。也就是說read與load之間、store與write之間是可插入其他指令的,如對主內(nèi)存中的變量a、b進(jìn)行訪問時,一種可能出現(xiàn)的順序是read a、read b、load b、 load a

Java內(nèi)存操作的規(guī)定
  • 不允許read和load、store和write操作之一單獨出現(xiàn),即不允許一個變量從主內(nèi)存讀取了但工作內(nèi)存不接受,或者從工作內(nèi)存發(fā)起回寫了但主內(nèi)存不接受的情況出現(xiàn)。
  • 不允許一個線程丟棄它的最近的assign操作,即變量在工作內(nèi)存中改變了之后必須把該變化同步回主內(nèi)存。
  • 不允許一個線程無原因地(沒有發(fā)生過任何assign操作)把數(shù)據(jù)從線程的工作內(nèi)存同步回主內(nèi)存。
  • 一個新的變量只能在主內(nèi)存中“誕生”,不允許在工作內(nèi)存中直接使用一個未被初始化(load或assign)的變量。
  • 一個變量在同一個時刻只允許一條線程對其進(jìn)行l(wèi)ock操作,但lock操作可以被同一條線程重復(fù)執(zhí)行多次,多次執(zhí)行l(wèi)ock后,只有執(zhí)行相同次數(shù)的unlock操作,變量才會被解鎖。
  • 如果對一個變量執(zhí)行l(wèi)ock操作,將會清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個變量前,需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值。
  • 如果一個變量事先沒有被lock操作鎖定,則不允許對它執(zhí)行unlock操作;也不允許去unlock一個被其他線程鎖定的變量。
  • 對一個變量執(zhí)行unlock操作之前,必須把此變量同步回主內(nèi)存中(執(zhí)行store和write操作)。
對于volatile型變量的特殊規(guī)則

關(guān)鍵字volatile可以說是Java虛擬機提供的最輕量級的同步機制。Java內(nèi)存模型對volatile專門定義了一些特殊的訪問規(guī)則。
當(dāng)一個變量被定義成volatile之后,它將具備兩種特性:

  1. 保證此變量對所有線程的可見性,這里的“可見性”是指當(dāng)一條線程修改了這個變量的值,新值對于其他線程來說是可以立即得知的。

由于volatile變量只能保證可見性,在不符合以下兩條規(guī)則的運算場景中,我們依然要通過加鎖來保證原子性。

  1. 運算結(jié)果并不依賴變量的當(dāng)前值,或者能夠確保只有單一的線程修改變量的值。
  2. 變量不需要與其他的狀態(tài)變量共同參與不變約束。
    如下代碼示例就很適合volatile變量來控制并發(fā)
volatile boolean isShutdown;
public void shutdown() {
 isShutdown = true
}

public void doWork() {
   while(!isShutdown) {
       // do something
   }
}
  1. 禁止指令重排序優(yōu)化,普通的變量僅僅會保證在該方法的執(zhí)行過程中所有依賴賦值結(jié)果的地方都能獲取到正確的結(jié)果,而不能保證變量賦值操作的順序與程序代碼中的執(zhí)行順序一致。
volatile性能
  1. volatile同步機制的性能要優(yōu)于鎖(使用synchronized關(guān)鍵字或java.util.concurrent包里面的鎖);
  2. volatile變量讀操作的性能消耗與普通變量幾乎沒有什么差別,但是寫操作則可能會慢上一些,因為它需要在本地代碼中插入許多內(nèi)存屏障(Memory Barrier 或Memory Fence)指令來保證處理器不發(fā)生亂序。
對于long和double型變量的特殊規(guī)則

Java內(nèi)存模型要求lock、unlock、read、load、assign、use、store和write這八個操作都具有原子性,但是對于64位的數(shù)據(jù)類型(long和double),在模型中特別定義一條寬松的規(guī)定:允許虛擬機將沒有被volatile修飾的64位數(shù)據(jù)的讀寫操作劃分為兩次32位的操作來進(jìn)行,也就是允許虛擬機實現(xiàn)選擇可以不保證64位數(shù)據(jù)類型的load、store、read和write這四個操作的原子性。
如果有多個線程共享一個并未聲明為volatile的long或double類型的變量,并且同時對它們進(jìn)行讀取和修改操作,那么某些線程可能會讀取到一個即非原值,也不是其他線程修改值的代表了半個變量的數(shù)值。
不過這種讀取到半個變量的情況非常罕見,因為Java內(nèi)存模型雖然允許虛擬機不把long和double變量的讀寫實現(xiàn)成原子操作,但允許虛擬機把這些操作實現(xiàn)為具有原子性的操作,而且還強烈建議虛擬機這樣實現(xiàn)。
目前各種估測下的商用虛擬機幾乎都選擇把64位數(shù)據(jù)的讀寫操作作為原子操作對待,因此我們在編碼是不需要將long和double變量專門聲明為volatile。

原子性、可見性與有序性

Java內(nèi)存模型是圍繞著在并發(fā)過程中如何處理原子性、可見性和有序性這三個特征來建立的,我們逐個來看一下哪些操作實現(xiàn)了這三個特性。

  • 原子性(Atomicity) - 由Java內(nèi)存模型來直接保證的原子性變量操作包括read、load、assign、use、store和write這六個,我們大致可以認(rèn)為基本數(shù)據(jù)類型的訪問讀寫是具備原子性的。如果需要一個更大范圍的原子性保證,sychronized關(guān)鍵字同步塊提供了這類操作(隱式插入字節(jié)碼指令monitorenter和monitorexit)。
  • 可見性(Visibility) - 可見性就是指當(dāng)一個線程修改了共享變量的值,其他線程能夠立即得知這個修改。上面我們已經(jīng)講過了volatile具有這個能力,除了volatile之外,Java還有兩個關(guān)鍵字能實現(xiàn)可見性,即synchronizedfinal。同步塊(synchronized)的可見性是由”對一個變量執(zhí)行unlock操作之前,必須先把此變量同步回主內(nèi)存中”這條規(guī)則獲得的,而final關(guān)鍵字的可見性是指:被final修飾的字段在構(gòu)造器中一旦被初始化完成,那么其他線程中就能看到final字段的值。
  • 有序性(Ordering) - Java內(nèi)存模型的有序性已經(jīng)在前面講解volatile時詳細(xì)討論過了,Java程序中天然的有序性可以總結(jié)為一句話:如果在本線程內(nèi)觀察,所有的操作都是有序的;如果在一個線程中觀察另一個線程,所有的操作都是無序的。前半句是指線程內(nèi)表現(xiàn)為串行的語義,后半句是指指令重排現(xiàn)象和工作內(nèi)存與主內(nèi)存同步延遲現(xiàn)象。

PS: 通過上面我們發(fā)現(xiàn)synchronized關(guān)鍵字在需要這三種特性時都可以作為其中一種的解決方案,看起來很萬能,這也是間接造就它被濫用的局面。

先行發(fā)生原則

如果Java內(nèi)存模型中所有的有序性都只能靠volatilesynchronized來完成,那么有些操作將會變得很啰嗦,但是我們在編寫Java并發(fā)代碼時并沒有感覺到這一點,這是因為Java語言中有一個先行發(fā)生(happens-before)的原則。
這個原則非常重要,它是判斷數(shù)據(jù)是否存在競爭,線程是否安全的主要依據(jù)。
下面是Java內(nèi)存模型下一些天然的先行發(fā)生關(guān)系,這些先行發(fā)生關(guān)系無須任何同步器協(xié)助就已經(jīng)存在,可以在編碼中直接使用。如果兩個操作之間的關(guān)系不在此列,并且無法從下列規(guī)則推導(dǎo)出來的話,它們就沒有順序性保障,虛擬機可以對它們進(jìn)行隨意重排序。

  • 程序次序規(guī)則(Program Order Rule) - 在一個線程內(nèi),按照程序代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作。
  • 管和鎖定規(guī)則(Monitor Lock Rule) - 一個unlock操作先行發(fā)生于后面對同一個鎖的lock操作。
  • volatile變量規(guī)則(Volatile Variable Rule) - 對一個volatile變量的寫操作先行發(fā)生于后面對這個變量的讀操作。
  • 線程啟動規(guī)則(Thread Start Rule) - Thread對象的start()方法先行發(fā)生于此線程的每一個動作。
  • 線程終止規(guī)則(Thread Termination Rule) - 線程中的所有操作都先行發(fā)生于對此線程的終止檢測,我們可以通過Thread.join()方法結(jié)束、ThreadisAlive()的返回值等手段檢測到線程已經(jīng)終止執(zhí)行。
  • 線程中斷規(guī)則(Thread Interruption Rule) - 對線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生,可以通過Thread.interrupted()方法檢測到是否有中斷發(fā)生。
  • 對象終結(jié)規(guī)則(Finalizer Rule) - 一個對象的初始化完成先行發(fā)生于它的finalize()方法的開始。
  • 傳遞性(Transitivity) - 如果操作A先行發(fā)生于操作B,操作B先行發(fā)生于操作C,那就可以得出操作A先行發(fā)生于操作C的結(jié)論。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容