必須要學(xué)會(huì)的JMM與volatile
1. JAVA 內(nèi)存模型 (JMM)
- JMM是用來(lái)干嘛的?:《Java虛擬機(jī)規(guī)范》中曾試圖定義一種“Java內(nèi)存模型”(Java Memory Model,JMM)來(lái)屏蔽各種硬件和操作系統(tǒng)的內(nèi)存訪問(wèn)差異,以實(shí)現(xiàn)讓Java程序在各種平臺(tái)下都能達(dá)到一致的內(nèi)存訪問(wèn)效果。
- 主要目的是什么?:定義程序中各種變量的訪問(wèn)規(guī)則,即關(guān)注在虛擬機(jī)中把變量值存儲(chǔ)到內(nèi)存和從內(nèi)存中取出變量值這樣的底層細(xì)節(jié)。此處的變量(Variables)與Java編程中所說(shuō)的變量有所區(qū)別,它包括了實(shí)例字段、靜態(tài)字段和構(gòu)成數(shù)組對(duì)象的元素,但是不包括局部變量與方法參數(shù),因?yàn)楹笳呤蔷€程私有的,不會(huì)被共享,自然就不會(huì)存在競(jìng)爭(zhēng)問(wèn)題。
1.1 主內(nèi)存與工作內(nèi)存

Java內(nèi)存模型規(guī)定了所有的變量都存儲(chǔ)在主內(nèi)存(Main Memory)中。每條線程還有自己的工作內(nèi)存(Working Memory),線程的工作內(nèi)存中保存了被該線程使用的變量的主內(nèi)存副本,線程對(duì)變量的所有操作(讀取、賦值等)都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫主內(nèi)存中的數(shù)據(jù)。不同的線程之間也無(wú)法直接訪問(wèn)對(duì)方工作內(nèi)存中的變量,線程間變量值的傳遞均需要通過(guò)主內(nèi)存來(lái)完成。
這里所講的主內(nèi)存、工作內(nèi)存與Java內(nèi)存區(qū)域中的Java堆、棧、方法區(qū)等并不是同一個(gè)層次的對(duì)內(nèi)存的劃分,這兩者基本上是沒(méi)有任何關(guān)系的。如果兩者一定要勉強(qiáng)對(duì)應(yīng)起來(lái),那么從變量、主內(nèi)存、工作內(nèi)存的定義來(lái)看,主內(nèi)存主要對(duì)應(yīng)于Java堆中的對(duì)象實(shí)例數(shù)據(jù)部分,而工作內(nèi)存則對(duì)應(yīng)于虛擬機(jī)棧中的部分區(qū)域。
從更基礎(chǔ)的層次上說(shuō),主內(nèi)存直接對(duì)應(yīng)于物理硬件的內(nèi)存,而為了獲取更好的運(yùn)行速度,虛擬機(jī)(或者是硬件、操作系統(tǒng)本身的優(yōu)化措施)可能會(huì)讓工作內(nèi)存優(yōu)先存儲(chǔ)于寄存器和高速緩存中,因?yàn)槌绦蜻\(yùn)行時(shí)主要訪問(wèn)的是工作內(nèi)存。
1.2 內(nèi)存間的交互
關(guān)于主內(nèi)存與工作內(nèi)存之間具體的交互協(xié)議,即一個(gè)變量如何從主內(nèi)存拷貝到工作內(nèi)存、如何從工作內(nèi)存同步回主內(nèi)存這一類的實(shí)現(xiàn)細(xì)節(jié),Java內(nèi)存模型中定義了以下8種操作來(lái)完成。Java虛擬機(jī)實(shí)現(xiàn)時(shí)必須保證下面提及的每一種操作都是原子的、不可再分的(對(duì)于double和long類型的變量來(lái)說(shuō),load、store、read和write操作在某些平臺(tái)上允許有例外)。
- lock(鎖定):作用于主內(nèi)存的變量,它把一個(gè)變量標(biāo)識(shí)為一條線程獨(dú)占的狀態(tài)。
- unlock(解鎖):作用于主內(nèi)存的變量,它把一個(gè)處于鎖定狀態(tài)的變量釋放出來(lá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)存中得到的變量的值放入主內(nèi)存的變量中。
非原子性協(xié)定:
Java內(nèi)存模型要求lock、unlock、read、load、assign、use、store、write這八種操作都具有原子性,但是對(duì)于64位的數(shù)據(jù)類型(long和double),在模型中特別定義了一條寬松的規(guī)定:允許虛擬機(jī)將沒(méi)有被volatile修飾的64位數(shù)據(jù)的讀寫操作劃分為兩次32位的操作來(lái)進(jìn)行,即允許虛擬機(jī)實(shí)現(xiàn)自行選擇是否要保證64位數(shù)據(jù)類型的load、store、read和write這四個(gè)操作的原子性,這就是所謂的“long和double的非原子性協(xié)定”(Non-Atomic Treatment of double and long Variables)。
如果要把一個(gè)變量從主內(nèi)存拷貝到工作內(nèi)存,那就要按順序執(zhí)行read和load操作,如果要把變量從工作內(nèi)存同步回主內(nèi)存,就要按順序執(zhí)行store和write操作。注意,Java內(nèi)存模型只要求上述兩個(gè)操作必須按順序執(zhí)行,但不要求是連續(xù)執(zhí)行。也就是說(shuō)read與load之間、store與write之間是可插入其他指令的,如對(duì)主內(nèi)存中的變量a、b進(jìn)行訪問(wèn)時(shí),一種可能出現(xiàn)的順序是read a、read b、load b、load a。
Java內(nèi)存模型還規(guī)定了在執(zhí)行上述8種基本操作時(shí)必須滿足如下規(guī)則:
- 不允許read和load、store和write操作之一單獨(dú)出現(xiàn),即不允許一個(gè)變量從主內(nèi)存讀取了但工作內(nèi)存不接受,或者工作內(nèi)存發(fā)起回寫了但主內(nèi)存不接受的情況出現(xiàn)。
- 不允許一個(gè)線程丟棄它最近的assign操作,即變量在工作內(nèi)存中改變了之后必須把該變化同步回主內(nèi)存。
- 不允許一個(gè)線程無(wú)原因地(沒(méi)有發(fā)生過(guò)任何assign操作)把數(shù)據(jù)從線程的工作內(nèi)存同步回主內(nèi)存中。
- 一個(gè)新的變量只能在主內(nèi)存中“誕生”,不允許在工作內(nèi)存中直接使用一個(gè)未被初始化(load或assign)的變量,換句話說(shuō)就是對(duì)一個(gè)變量實(shí)施use、store操作之前,必須先執(zhí)行assign和load操作。
- 一個(gè)變量在同一個(gè)時(shí)刻只允許一條線程對(duì)其進(jìn)行l(wèi)ock操作,但lock操作可以被同一條線程重復(fù)執(zhí)行多次,多次執(zhí)行l(wèi)ock后,只有執(zhí)行相同次數(shù)的unlock操作,變量才會(huì)被解鎖。
- 如果對(duì)一個(gè)變量執(zhí)行l(wèi)ock操作,那將會(huì)清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個(gè)變量前,需要重新執(zhí)行l(wèi)oad或assign操作以初始化變量的值。
- 如果一個(gè)變量事先沒(méi)有被lock操作鎖定,那就不允許對(duì)它執(zhí)行unlock操作,也不允許去unlock一個(gè)被其他線程鎖定的變量。
- 對(duì)一個(gè)變量執(zhí)行unlock操作之前,必須先把此變量同步回主內(nèi)存中(執(zhí)行store、write操作)。
2. 關(guān)于 Volatile 變量
Volatile變量具備的三個(gè)關(guān)鍵點(diǎn)(保證可見(jiàn)性,不能保證原子性,禁止指令重排序):
- 用volatile聲明一個(gè)變量可以保證對(duì)所有線程的可見(jiàn)性,這里的“可見(jiàn)性”是指當(dāng)一條線程修改了這個(gè)變量的值,新值對(duì)于其他線程來(lái)說(shuō)是可以及時(shí)得知的(并不是立即可見(jiàn)的,從物理存儲(chǔ)的角度看,各個(gè)線程的工作內(nèi)存中volatile變量也可以存在不一致的情況,但由于每次使用之前都要先刷新,執(zhí)行引擎看不到不一致的情況,因此可以認(rèn)為不存在一致性問(wèn)題))。
- 基于volatile變量的運(yùn)算在并發(fā)下并不是線程安全的。
- 禁止指令重排序優(yōu)化,普通的變量?jī)H會(huì)保證在該方法的執(zhí)行過(guò)程中所有依賴賦值結(jié)果的地方都能獲取到正確的結(jié)果,而不能保證變量賦值操作的順序與程序代碼中的執(zhí)行順序一致。因?yàn)樵谕粋€(gè)線程的方法執(zhí)行過(guò)程中無(wú)法感知到這點(diǎn),這就是Java內(nèi)存模型中描述的所謂“線程內(nèi)表現(xiàn)為串行的語(yǔ)義”(As-If-Serial)。
對(duì)于 As-If-Serial 的簡(jiǎn)要說(shuō)明:對(duì)于處理器或者編譯器來(lái)說(shuō),在進(jìn)行指令重排序優(yōu)化(為了提高并行度)的時(shí)候只能保證在單線程環(huán)境下的串行化語(yǔ)義的一致性。
舉個(gè)栗子,下面一個(gè)雙鎖檢測(cè)(Double Check Lock,DCL)單例:
public class Singleton {
private volatile static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
public static void main(String[] args) {
Singleton.getInstance();
}
}對(duì)instance變量賦值相關(guān)的字節(jié)碼:
0x01a3de0f: mov $0x3375cdb0,%esi ;...beb0cd75 33
; {oop('Singleton')}
0x01a3de14: mov %eax,0x150(%esi) ;...89865001 0000
0x01a3de1a: shr $0x9,%esi ;...c1ee09
0x01a3de1d: movb $0x0,0x1104800(%esi) ;...c6860048 100100
0x01a3de24: lock addl $0x0,(%esp) ;...f0830424 00
;*putstatic instance
; - Singleton::getInstance@24有volatile修飾的變量,賦值后(前面mov%eax,0x150(%esi)這句便是賦值操作)多執(zhí)行了一個(gè) lock addl$0x0,(%esp) 操作,這個(gè)操作的作用相當(dāng)于一個(gè)內(nèi)存屏障 (Memory Barrier或Memory Fence,指重排序時(shí)不能把后面的指令重排序到內(nèi)存屏障之前的位置),只有一個(gè)處理器訪問(wèn)內(nèi)存時(shí),并不需要內(nèi)存屏障;但如果有兩個(gè)或更多處理器訪問(wèn)同一塊內(nèi)存,且其中有一個(gè)在觀測(cè)另一個(gè),就需要內(nèi)存屏障來(lái)保證一致性了。
這句指令中的 addl$0x0,(%esp) (把ESP寄存器的值加0)顯然是一個(gè)空操作,之所以用這個(gè)空操作而不是空操作專用指令nop,是因?yàn)镮A32手冊(cè)規(guī)定lock前綴不允許配合nop指令使用。這里的關(guān)鍵在于lock前綴,查詢IA32手冊(cè)可知,它的作用是將本處理器的緩存寫入了內(nèi)存,該寫入動(dòng)作也會(huì)引起別的處理器或者別的內(nèi)核無(wú)效化(Invalidate)其緩存,這種操作相當(dāng)于對(duì)緩存中的變量做了一次前面介紹Java內(nèi)存模式中所說(shuō)的“store和write”操作。所以通過(guò)這樣一個(gè)空操作,可讓前面volatile變量的修改對(duì)其他處理器立即可見(jiàn)。
那為何說(shuō)它禁止指令重排序呢?從硬件架構(gòu)上講,指令重排序是指處理器采用了允許將多條指令不按程序規(guī)定的順序分開發(fā)送給各個(gè)相應(yīng)的電路單元進(jìn)行處理。但并不是說(shuō)指令任意重排,處理器必須能正確處理指令依賴情況保障程序能得出正確的執(zhí)行結(jié)果。譬如指令1把地址A中的值加10,指令2把地址A中的值乘以2,指令3把地址B中的值減去3,這時(shí)指令1和指令2是有依賴的,它們之間的順序不能重排—— (A+10)*2 與 A*2+10 顯然不相等,但指令3可以重排到指令1、2之前或者中間,只要保證處理器執(zhí)行后面依賴到A、B值的操作時(shí)能獲取正確的A和B值即可。所以在同一個(gè)處理器中,重排序過(guò)的代碼看起來(lái)依然是有序的。因此,lock addl$0x0,(%esp) 指令把修改同步到內(nèi)存時(shí),意味著所有之前的操作都已經(jīng)執(zhí)行完成,這樣便形成了“指令重排序無(wú)法越過(guò)內(nèi)存屏障”的效果。
假定T表示一個(gè)線程,V和W分別表示兩個(gè)volatile型變量,那么在進(jìn)行read、load、use、assign、store和write操作時(shí)需要滿足如下對(duì)于volatile變量的特殊規(guī)則:
- 只有當(dāng)線程T對(duì)變量V執(zhí)行的前一個(gè)動(dòng)作是load的時(shí)候,線程T才能對(duì)變量V執(zhí)行use動(dòng)作;并且,只有當(dāng)線程T對(duì)變量V執(zhí)行的后一個(gè)動(dòng)作是use的時(shí)候,線程T才能對(duì)變量V執(zhí)行l(wèi)oad動(dòng)作。線程T對(duì)變量V的use動(dòng)作可以認(rèn)為是和線程T對(duì)變量V的load、read動(dòng)作相關(guān)聯(lián)的,必須連續(xù)且一起出現(xiàn)。(及時(shí)得到volatile變量的新值)
- 只有當(dāng)線程T對(duì)變量V執(zhí)行的前一個(gè)動(dòng)作是assign的時(shí)候,線程T才能對(duì)變量V執(zhí)行store動(dòng)作;并且,只有當(dāng)線程T對(duì)變量V執(zhí)行的后一個(gè)動(dòng)作是store的時(shí)候,線程T才能對(duì)變量V執(zhí)行assign動(dòng)作。線程T對(duì)變量V的assign動(dòng)作可以認(rèn)為是和線程T對(duì)變量V的store、write動(dòng)作相關(guān)聯(lián)的,必須連續(xù)且一起出現(xiàn)。(及時(shí)將volatile變量的改動(dòng)同步到主存)
- 假定動(dòng)作A是線程T對(duì)變量V實(shí)施的use或assign動(dòng)作,假定動(dòng)作F是和動(dòng)作A相關(guān)聯(lián)的load或store動(dòng)作,假定動(dòng)作P是和動(dòng)作F相應(yīng)的對(duì)變量V的read或write動(dòng)作;與此類似,假定動(dòng)作B是線程T對(duì)變量W實(shí)施的use或assign動(dòng)作,假定動(dòng)作G是和動(dòng)作B相關(guān)聯(lián)的load或store動(dòng)作,假定動(dòng)作Q是和動(dòng)作G相應(yīng)的對(duì)變量W的read或write動(dòng)作。如果A先于B,那么P先于Q。(禁止指令重排序優(yōu)化)
3. 關(guān)于內(nèi)存屏障
內(nèi)存屏障又稱內(nèi)存柵欄(Memory Barrier)是一個(gè)CPU指令,它的作用有兩個(gè):
- 保證特定操作的執(zhí)行順序
- 保證某些變量的內(nèi)存可見(jiàn)性(利用該特性實(shí)現(xiàn)volatile的內(nèi)存可見(jiàn)性)
由于編譯器和處理器都能執(zhí)行指令重排優(yōu)化。如果在指令間插入一條Memory Barrier則會(huì)告訴編譯器和CPU,不管什么指令都不能和這條Memory Barrier指令重排序,也就是說(shuō)通過(guò)插入內(nèi)存屏障禁止在內(nèi)存屏障前后的指令執(zhí)行重排序優(yōu)化。Memory Barrier的另外一個(gè)作用是強(qiáng)制刷出各種CPU的緩存數(shù)據(jù),因此任何CPU。上的線程都能讀取到這些數(shù)據(jù)的最新版本??傊畍olatile變量正是通過(guò)內(nèi)存屏障實(shí)現(xiàn)其在內(nèi)存中的語(yǔ)義,即可見(jiàn)性和禁止重排優(yōu)化。
Intel硬件提供了一系列的內(nèi)存屏障, 主要有:
- Ifence,是一種Load Barrier讀屏障
- sfence,是一種Store Barrier寫屏障
- mfence,是一種全能型的屏障, 具備ifence和sfence的能力
- Lock前綴, Lock不是一種內(nèi)存屏障,但是它能完成類似內(nèi)存屏障的功能。Lock會(huì)對(duì)CPU總線和高速緩存加鎖,可以理解為CPU指令級(jí)的一種鎖。它后面可以跟ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG等指令。
不同硬件實(shí)現(xiàn)內(nèi)存屏障的方式不同,Java內(nèi)存模型屏蔽了這種底層硬件平臺(tái)的差異,由JVM來(lái)為不同的平臺(tái)生成相應(yīng)的機(jī)器碼。
JVM中提供了四類內(nèi)存屏障指令:

volatile內(nèi)存語(yǔ)義的實(shí)現(xiàn):

總的來(lái)說(shuō):
- 當(dāng)?shù)谝粋€(gè)操作是volatile讀時(shí),不管第二個(gè)操作是什么,都不能重排序。這個(gè)規(guī)則確保volatile讀之后的操作不會(huì)被編譯器重排序到volatile讀之前。
- 當(dāng)?shù)诙€(gè)操作是volatile寫時(shí),不管第一個(gè)操作是什么, 都不能重排序。這個(gè)規(guī)則確保volatile寫之前的操作不會(huì)被編譯器重排序到volatile寫之后。
- 當(dāng)?shù)谝粋€(gè)操作是volatile寫,第二個(gè)操作是volatile讀或?qū)憰r(shí),不能重排序。
為了實(shí)現(xiàn)volatile的內(nèi)存語(yǔ)義,編譯器在生成字節(jié)碼時(shí),會(huì)在指令序列中插入內(nèi)存屏障來(lái)禁止特定類型的處理器重排序。對(duì)于編譯器來(lái)說(shuō),發(fā)現(xiàn)一個(gè)最優(yōu)布置來(lái)最小化插入屏障的總數(shù)幾乎不可能。為此, JMM采取保守策略
下面是基于保守策略的JMM內(nèi)存屏障插入策略:
- 在每個(gè)volatile寫操作的前面插入一個(gè)StoreStore屏障。
- 在每個(gè)volatile寫操作的后面插入一個(gè)StoreLoad屏障。
- 在每個(gè)volatile讀操作的后面插入一個(gè)LoadLoad屏障。
- 在每個(gè)volatile讀操作的后面插入一個(gè)LoadStore屏障。
上述內(nèi)存屏障插入策略非常保守,但它可以保證在任意處理器平臺(tái),任意的程序中都能得到正確的volatile內(nèi)存語(yǔ)義。

上圖中StoreStore屏障可以保證在volatile寫之前,其前面的所有普通寫操作已經(jīng)對(duì)任意處理器可見(jiàn)了。這是因?yàn)镾toreStore屏障將保障 上面所有的普通寫在volatile寫之前刷新到主內(nèi)存。
上圖中StoreLoad屏障的作用是避免volatile寫與后面可能有的 volatile讀/寫 操作重排序。因?yàn)榫幾g器常常無(wú)法準(zhǔn)確判斷在一個(gè)volatile寫的后面 否需要插入一個(gè)StoreLoad屏障(比如,一個(gè)volatile寫之后方法立即return)。為了保證能正確實(shí)現(xiàn)volatile的內(nèi)存語(yǔ)義,JMM在采取了保守策略:在每個(gè)volatile寫的后面,或者在每個(gè)volatile讀的前面插入一個(gè)StoreLoad屏障。
從整體執(zhí)行效率的角度考慮,JMM最終選擇了在每個(gè)volatile寫的后面插入一個(gè)StoreLoad屏障。因?yàn)関olatile寫-讀內(nèi)存語(yǔ)義的常見(jiàn)使用模式是:一個(gè)寫線程寫volatile變量,多個(gè)讀線程讀同一個(gè)volatile變量。 當(dāng)讀線程的數(shù)量大大超過(guò)寫線程時(shí),選擇在volatile寫之后插入StoreLoad屏障將帶來(lái)可觀的執(zhí)行效率的提升。從這里可以看到JMM在實(shí)現(xiàn)上的一個(gè)特點(diǎn):首先確保正確性,然后再去追求執(zhí)行效率。

上圖中LoadLoad屏障用來(lái)禁止處理器把上面的volatile讀與下面的普通讀重排序。LoadStore屏障用來(lái)禁 止處理器把上面的volatile讀與下面的普通寫重排序。
class VolatileBarrierExample {
int a;
volatile int v1 = 1;
volatile int v2 = 2;
void readAndwrite() {
inti = v1; //第一個(gè)volatile讀
intj = v2 //第二個(gè)volatile讀
a = i+j; // 普通寫
v1 = i+ 1; //第一個(gè)volatile寫
v2=j * 2; //第二個(gè)volatile寫
}
}針對(duì)readAndWrite()方法,編譯器在生成字節(jié)碼時(shí)可以做如下的優(yōu)化。

注意,最后的StoreLoad屏障不能省略。因?yàn)榈诙€(gè)volatile寫之后, 方法立即return。 此時(shí)編譯器可能無(wú)法準(zhǔn)確斷定后面是否會(huì)有volatile讀或?qū)?,為了安全起?jiàn)編譯器通常會(huì)在這里插入一個(gè)StoreLoad屏障。上面的優(yōu)化針對(duì)任意處理器平臺(tái),由于不同的處理器有不同“松緊度"的處理器內(nèi)存模型,內(nèi)存屏障的插入還可以根據(jù)具體的處理器內(nèi)存模型繼續(xù)優(yōu)化。
4. 原子性、可見(jiàn)性與有序性
保證原子性(Atomicity):
- JMM定義的原子操作:JMM來(lái)直接保證的原子性變量的操作包括read、load、assign、use、store和write這六個(gè)。
- synchronized關(guān)鍵字:Java內(nèi)存模型還提供了lock和unlock操作來(lái)滿足更大范圍的原子性保證,盡管虛擬機(jī)未把lock和unlock操作直接開放給用戶使用,但是卻提供了更高層次的字節(jié)碼指令monitorenter和monitorexit來(lái)隱式地使用這兩個(gè)操作。反映到Java代碼中就是同步塊synchronized關(guān)鍵字,因此在synchronized塊之間的操作也具備原子性。
保證可見(jiàn)性(Visibility):
- volatile關(guān)鍵字:JMM對(duì)于volatile定義的特殊規(guī)則保證了新值能立即同步到主內(nèi)存,以及每次使用前立即從主內(nèi)存刷新來(lái)保證可見(jiàn)性的。
- synchronized關(guān)鍵字:同步塊的可見(jiàn)性是由“對(duì)一個(gè)變量執(zhí)行unlock操作之前,必須先把此變量同步回主內(nèi)存中(執(zhí)行store、write操作)”這條規(guī)則獲得的。
- final關(guān)鍵字:被final修飾的字段在構(gòu)造器中一旦被初始化完成,并且構(gòu)造器沒(méi)有把“this”的引用傳遞出去(this引用逃逸是一件很危險(xiǎn)的事情,其他線程有可能通過(guò)這個(gè)引用訪問(wèn)到“初始化了一半”的對(duì)象),那么在其他線程中就能看見(jiàn)final字段的值。
保證有序性(Ordering):
- As-If-Serial:如果在本線程內(nèi)觀察,所有的操作都是有序的。
- volatile關(guān)鍵字:volatile關(guān)鍵字本身就包含了禁止指令重排序的語(yǔ)義,在具體的實(shí)現(xiàn)中(依賴內(nèi)存屏障)就會(huì)保證指令的有序性。
- synchronized關(guān)鍵字:“一個(gè)變量在同一個(gè)時(shí)刻只允許一條線程對(duì)其進(jìn)行l(wèi)ock操作”這條規(guī)則決定了持有同一個(gè)鎖的兩個(gè)同步塊只能串行地進(jìn)入而保證執(zhí)行時(shí)的有序性。
5. Happens-Before
先行發(fā)生是(Happens-Before) Java內(nèi)存模型中定義的兩項(xiàng)操作之間的偏序關(guān)系,比如說(shuō)操作A先行發(fā)生于操作B,其實(shí)就是說(shuō)在發(fā)生操作B之前,操作A產(chǎn)生的影響能被操作B觀察到,“影響”包括修改了內(nèi)存中共享變量的值、發(fā)送了消息、調(diào)用了方法等。(先行發(fā)生原則是JMM的實(shí)現(xiàn)所體現(xiàn)出的一些特定的現(xiàn)象)Java語(yǔ)言無(wú)須任何同步手段保障就能成立的先行發(fā)生規(guī)則有且只有下面這些:
- 程序次序規(guī)則(Program Order Rule):在一個(gè)線程內(nèi),按照控制流順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作。注意,這里說(shuō)的是控制流順序而不是程序代碼順序,因?yàn)橐紤]分支、循環(huán)等結(jié)構(gòu)。
- 管程鎖定規(guī)則(Monitor Lock Rule):一個(gè)unlock操作先行發(fā)生于后面對(duì)同一個(gè)鎖的lock操作。這里必須強(qiáng)調(diào)的是“同一個(gè)鎖”,而“后面”是指時(shí)間上的先后。
- volatile變量規(guī)則(Volatile Variable Rule):對(duì)一個(gè)volatile變量的寫操作先行發(fā)生于后面對(duì)這個(gè)變量的讀操作,這里的“后面”同樣是指時(shí)間上的先后。
- 線程啟動(dòng)規(guī)則(Thread Start Rule):Thread對(duì)象的start()方法先行發(fā)生于此線程的每一個(gè)動(dòng)作。
- 線程終止規(guī)則(Thread Termination Rule):線程中的所有操作都先行發(fā)生于對(duì)此線程的終止檢測(cè),我們可以通過(guò)Thread::join()方法是否結(jié)束、Thread::isAlive()的返回值等手段檢測(cè)線程是否已經(jīng)終止執(zhí)行。
- 線程中斷規(guī)則(Thread Interruption Rule):對(duì)線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生,可以通過(guò)Thread::interrupted()方法檢測(cè)到是否有中斷發(fā)生。
- 對(duì)象終結(jié)規(guī)則(Finalizer Rule):一個(gè)對(duì)象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)先行發(fā)生于它的finalize()方法的開始。
- 傳遞性(Transitivity):如果操作A先行發(fā)生于操作B,操作B先行發(fā)生于操作C,那就可以得出操作A先行發(fā)生于操作C的結(jié)論。
舉個(gè)栗子:
private int value = 0;
pubilc void setValue(int value){
this.value = value;
}
public int getValue(){
return value;
}假設(shè)存在線程A和B,線程A先(時(shí)間上的先后)調(diào)用了setValue(1),然后線程B調(diào)用了同一個(gè)對(duì)象的getValue(),那么線程B收到的返回值是什么?
根據(jù)先行發(fā)生原則中的各項(xiàng)規(guī)則來(lái)進(jìn)行判斷:
- 由于兩個(gè)方法分別由線程A和B調(diào)用,不在一個(gè)線程中,所以程序次序規(guī)則在這里不適用;
- 由于沒(méi)有同步塊,自然就不會(huì)發(fā)生lock和unlock操作,所以管程鎖定規(guī)則不適用;
- 由于value變量沒(méi)有被volatile關(guān)鍵字修飾,所以volatile變量規(guī)則不適用;
- 后面的線程啟動(dòng)、終止、中斷規(guī)則和對(duì)象終結(jié)規(guī)則也和這里完全沒(méi)有關(guān)系;
- 因?yàn)闆](méi)有一個(gè)適用的先行發(fā)生規(guī)則,所以最后一條傳遞性也無(wú)法滿足;
因此我們可以判定,盡管線程A在操作時(shí)間上先于線程B,但是無(wú)法確定線程B中g(shù)etValue()方法的返回結(jié)果,換句話說(shuō),這里面的操作不是線程安全的。
再舉個(gè)栗子:
// 以下操作在同一個(gè)線程中執(zhí)行 int i = 1; int j = 2;
根據(jù)程序次序規(guī)則,“int i=1”的操作先行發(fā)生于“int j=2”,但是“int j=2”的代碼完全可能先被處理器執(zhí)行,這并不影響先行發(fā)生原則的正確性,因?yàn)槲覀冊(cè)谶@條線程之中沒(méi)有辦法感知到這一點(diǎn)。
到此這篇關(guān)于必須要學(xué)會(huì)的JMM與volatile的文章就介紹到這了,更多相關(guān)JMM與volatile內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring打包jar包時(shí)jsp頁(yè)面無(wú)法訪問(wèn)問(wèn)題解決
這篇文章主要介紹了Spring打包jar包時(shí)jsp頁(yè)面無(wú)法訪問(wèn)問(wèn)題解決,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-05-05
SpringBoot整合RocketMQ批量發(fā)送消息的實(shí)現(xiàn)代碼
這篇文章主要介紹了SpringBoot整合RocketMQ批量發(fā)送消息的實(shí)現(xiàn),文中通過(guò)代碼示例講解的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2024-04-04
Java IO流學(xué)習(xí)總結(jié)之文件傳輸基礎(chǔ)
這篇文章主要介紹了Java IO流學(xué)習(xí)總結(jié)之文件傳輸基礎(chǔ),文中有非常詳細(xì)的代碼示例,對(duì)正在學(xué)習(xí)java io流的小伙伴們有很好的幫助,需要的朋友可以參考下2021-04-04
一篇文章帶了解如何用SpringBoot在RequestBody中優(yōu)雅的使用枚舉參數(shù)
這篇文章主要介紹了SpringBoot中RequestBodyAdvice使用枚舉參數(shù),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08
struts2+spring+ibatis框架整合實(shí)現(xiàn)增刪改查
這篇文章主要為大家詳細(xì)介紹了struts2+spring+ibatis框架整合實(shí)現(xiàn)增刪改查操作,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-07-07
SpringBoot整合SpringBoot-Admin實(shí)現(xiàn)監(jiān)控應(yīng)用功能
本文主要介紹如何整合Spring Boot Admin,以此監(jiān)控Springboot應(yīng)用,文中有相關(guān)的示例代碼供大家參考,需要的朋友可以參考下2023-05-05
如何使用@AllArgsConstructor和final 代替 @Autowired
這篇文章主要介紹了使用@AllArgsConstructor和final 代替 @Autowired方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09

