Java內存模型(Java Memory Model,JMM)
1.主內存與工作內存
Java內存模型的主要目標是定義程序中各個變量的訪問規(guī)則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣的底層細節(jié)。此處的變量(Variables)與Java編程中所說的變量有所區(qū)別,它包括了實例字段、靜態(tài)字段和構成數(shù)組對象的元素,但不包括局部變量與方法參數(shù),因為后者是線程私有的,不會被共享,自然就不會存在競爭問題。為了獲得較好的執(zhí)行效能,Java內存模型并沒有限制執(zhí)行引擎使用處理器的特定寄存器或緩存來和主內存進行交互,也沒有限制即時編譯器進行調整代碼執(zhí)行順序這類優(yōu)化措施。
Java內存模型規(guī)定了所有的變量都存儲在主內存中。每條線程還有自己的工作內存,線程的工作內存中保存了被該線程使用到的變量的主內存副本拷貝,線程對變量的所有操作(讀取、賦值等)都必須在工作內存中進行,而不能直接讀寫主內存中的變量。不同的線程之間也無法直接訪問對方工作內存中的變量,線程間變量值的傳遞均需要通過主內存來完成,線程、主內存、工作內存三者的交互關系如圖所示。

2.內存間交互操作
關于主內存與工作內存之間具體的交互協(xié)議,即一個變量如何從主內存拷貝到工作內存、如何從工作內存同步回主內存之類的實現(xiàn)細節(jié),Java內存模型中定義了以下8種操作來完成,虛擬機實現(xiàn)時必須保證下面提及的每一種操作都是原子的/不可再分的。
- lock(鎖定):作用于主內存的變量,它把一個變量標識為一條線程獨占的狀態(tài)。
- unlock(解鎖):作用于主內存的變量,它把一個處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可以被其他線程鎖定。
- read(讀?。鹤饔糜谥鲀却娴淖兞浚岩粋€變量的值從主內存?zhèn)鬏數(shù)骄€程的工作內存的變量副本中。
- load(載入):作用于工作內存的變量,它把read操作從主內存中得到的變量值放入工作內存的變量副本中。
- use(使用):作用于工作內存的變量,它把工作內存中一個變量的值傳遞給執(zhí)行引擎,每當虛擬機遇到一個需要使用到變量的值的字節(jié)碼指令時將會執(zhí)行這個操作。
- assign(賦值):作用于工作內存的變量,它把一個執(zhí)行引擎接收到的值賦給工作內存的變量,每當虛擬機遇到一個給變量賦值的字節(jié)碼指令時執(zhí)行這個操作。
- store(存儲):作用于工作內存的變量,它把工作內存中一個變量的值傳送到主內存中,以便隨后的write操作使用。
- write(寫入):作用于主內存的變量,它把store操作從工作內存中得到的變量的值放入主內存的變量中。
如果要把一個變量從主內存復制到工作內存,那就要順序的執(zhí)行read和load操作,如果要把變量從工作內存同步回主內存,就要順序的執(zhí)行store和write操作。注意,Java內存模型只要求上述兩個操作必須按順序執(zhí)行,而沒有保證是連續(xù)執(zhí)行。也就是說,read和load之間、store和write之間是可插入其他指令的,如對主內存中的變量a、b進行訪問時,一種可能出現(xiàn)的順序是read a、read b、load b、load a。除此之外,Java內存模型還規(guī)定了在執(zhí)行上述8種基本操作時必須滿足如下規(guī)則:
- 不允許read和load、store和write操作之一單獨出現(xiàn),即不允許一個變量從主內存讀取了但工作內存不接受,或者從工作內存發(fā)起回寫了但主內存不接受的情況出現(xiàn)。
- 不允許一個線程丟棄它的最近的assign操作,即變量在工作內存中改變了之后必須把該變化同步回主內存。
- 不允許一個線程無原因的(沒有發(fā)生任何assign操作)把數(shù)據(jù)從線程的工作內存同步回主內存中。
- 一個新的變量只能在主內存中誕生,不允許在工作內存中直接使用一個未被初始化(load或assign)的變量,換句話說,就是對一個變量實施use、store操作之前,必須先執(zhí)行過來assign和load操作。
- 一個變量在同一個時刻只允許一條線程對其進行l(wèi)ock操作,但lock操作可以被同一條線程重復執(zhí)行多次,多次執(zhí)行l(wèi)ock后,只有執(zhí)行相同次數(shù)的unlock操作,變量才會被解鎖。
- 如果對一個變量執(zhí)行l(wèi)ock操作,那就會清空工作內存中此變量的值,在執(zhí)行引擎使用這個變量前,需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值。
- 如果一個變量事先沒有被lock操作鎖定,那就不允許對它執(zhí)行unlock操作,也不允許去unlock一個被其他線程鎖定住的變量。
- 對一個變量執(zhí)行unlock操作之前,必須先把此變量同步回到主內存中(執(zhí)行store、write操作)。
3.volatile型變量的特殊規(guī)則
當一個變量定義為volatile之后,它將具備兩種特性,第一是保證此變量對所有線程的可見性,這里的可見性是指當一條線程修改了這個變量的值,新值對于其他線程來說是可以立即得知的。而普通變量不能做到這一點,普通變量的值在線程間傳遞均需要通過主內存來完成。例如,線程A修改一個普通變量的值,然后向主內存進行回寫,另外一條線程B在線程A回寫完成之后再從主內存進行讀取操作,新變量值才會對線程B可見。
由于volatile變量只能保證可見性,在不符合以下兩條規(guī)則的運算場景中,我們仍然要通過加鎖(使用synchronized或java.util.concurrent中的原子類)來保證原子性。
- 運算結果并不依賴變量的當前值,或者能夠確保只有單一的線程修改變量的值。
- 變量不需要與其他的狀態(tài)變量共同參與不變約束。
使用volatile變量的第二個語義是禁止指令重排序優(yōu)化,普通的變量僅僅會保證在該方法的執(zhí)行過程中所有依賴賦值結果的地方都能獲取到正確的結果,而不能保證變量賦值操作的順序與程序代碼中的執(zhí)行順序一致。因為在一個線程的方法執(zhí)行過程中無法感知到這點,這就是Java內存模式中描述的所謂的“線程內表現(xiàn)為串行的語義”。
4.long和double型變量的特殊規(guī)則
Java內存模型要求lock、unlock、read、load、assign、use、store、write這8個操作都是原子性,但是對于64位的數(shù)據(jù)類型(long和double),在模型中特別定義了一個相對寬松的規(guī)定:允許虛擬機將沒有被volatile修飾的64位數(shù)據(jù)的讀寫操作劃分為兩次32位的操作來進行,即允許虛擬機實現(xiàn)選擇可以不保證64位數(shù)據(jù)類型的load、store、read和write這4個操作的原子性。
如果有多個線程共享一個并未聲明為volatile的long或double類型的變量,并且同時對它們進行讀取和修改操作,那么某些線程可能會讀取到一個既非原值,也不是其他線程修改值的代表了“半個變量”的數(shù)值。
5.原子性、可見性與有序性
原子性:由Java內存模型來直接保證的原子性變量操作包括read、load、assign、use、store和write,我們大致可以認為基本數(shù)據(jù)類型的訪問讀寫是具備原子性的。
如果應用場景需要一個更大范圍的原子性保證,Java內存模型還提供了lock和unlock操作來滿足這種需求,盡管虛擬機未把lock和unlock操作直接開放給用戶使用,但是卻提供了更高層次的字節(jié)碼指令monitorenter和monitorexit來隱式的使用這兩個操作,這兩個字節(jié)碼指令反映到Java代碼中就是同步塊——synchronized關鍵字,因此在synchronized塊之間的操作也具備原子性。
可見性:可見性是指當一個線程修改了共享變量的值,其他線程能夠立即得知這個修改。Java內存模型是通過在變量修改后將新值同步回主內存,在變量讀取前從主內存刷新變量值這種依賴主內存作為傳遞媒介的方式來實現(xiàn)可見性。volatile的特殊規(guī)則保證了新值能立即同步同主內存,以及每次使用前立即從主內存刷新。因此,volatile保證了多線程操作時變量的可見性。
除了volatile之外,Java還有兩個關鍵字能實現(xiàn)可見性,即synchronized和final。同步塊的可見性是由“對一個變量執(zhí)行unlock操作之前,必須先把此變量同步回主內存中(執(zhí)行store、write操作)”這條規(guī)則獲得的,而final關鍵字的可見性是指:被final修飾的字段在構造器中一旦初始化完成,并且構造器沒有把“this”的引用傳遞出去,那在其他線程中就能看見final字段的值。
有序性:Java程序中的有序性可以總結為一句話:如果在本線程內觀察,所有的操作都是有序的;如果在一個線程中觀察另一個線程,所有的操作都是無序的。前半句是指“線程內表現(xiàn)為串行語義”,后半句是指“指令重排序”現(xiàn)象和“工作內存與主內存同步延遲”現(xiàn)象。
Java語言提供了volatile和synchronized兩個關鍵字來保證線程之間操作的有序性,volatile關鍵字本身就包含了禁止指令重排序的語義,而synchronized則是由“一個變量在同一時刻只允許一條線程對其進行l(wèi)ock操作”這條規(guī)則獲得的,這條規(guī)則決定了持有同一個鎖的兩個同步塊只能串行的進入。
6.先行發(fā)生原則
先行發(fā)生是Java內存模型中定義的兩項操作之間的偏序關系,如果說操作A先行發(fā)生于操作B,其實就是說在發(fā)生操作B之前,操作A產生的影響能被操作B觀察到,“影響”包括修改了內存中共享變量的值、發(fā)送了消息、調用了方法等。下面是Java內存模型下一些“天然的”先行發(fā)生關系,這些先行發(fā)生關系無須任何同步器協(xié)助就已經存在,可以在編碼中直接使用。
- 程序次序規(guī)則:在一個線程內,按照程序代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作。
- 管程鎖定規(guī)則:一個unlock操作先行發(fā)生于后面對同一個鎖的lock操作。
- volatile變量規(guī)則:對一個volatile變量的寫操作先行發(fā)生于后面對這個變量的讀操作。
- 線程啟動規(guī)則:Thread對象的start()方法先行發(fā)生于此線程的每一個動作。
- 線程終止規(guī)則:線程中的所有操作都先行發(fā)生于對此線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值等手段檢測到線程已經終止執(zhí)行。
- 線程中斷規(guī)則:對線程interrupt()方法的調用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生,可以通過Thread.interrupted()方法檢測到是否有中斷發(fā)生。
- 對象終結規(guī)則:一個對象的初始化完成先行發(fā)生于它的finalize()方法的開始。
- 傳遞性:如果操作A先行發(fā)生于操作B,操作B先行發(fā)生于操作C,那就可以得出操作A先行發(fā)生于操作C的結論。