必須要學會的JMM與volatile
1. JAVA 內(nèi)存模型 (JMM)
- JMM是用來干嘛的?:《Java虛擬機規(guī)范》中曾試圖定義一種“Java內(nèi)存模型”(Java Memory Model,JMM)來屏蔽各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,以實現(xiàn)讓Java程序在各種平臺下都能達到一致的內(nèi)存訪問效果。
- 主要目的是什么?:定義程序中各種變量的訪問規(guī)則,即關(guān)注在虛擬機中把變量值存儲到內(nèi)存和從內(nèi)存中取出變量值這樣的底層細節(jié)。此處的變量(Variables)與Java編程中所說的變量有所區(qū)別,它包括了實例字段、靜態(tài)字段和構(gòu)成數(shù)組對象的元素,但是不包括局部變量與方法參數(shù),因為后者是線程私有的,不會被共享,自然就不會存在競爭問題。
1.1 主內(nèi)存與工作內(nèi)存
Java內(nèi)存模型規(guī)定了所有的變量都存儲在主內(nèi)存(Main Memory)中。每條線程還有自己的工作內(nèi)存(Working Memory),線程的工作內(nèi)存中保存了被該線程使用的變量的主內(nèi)存副本,線程對變量的所有操作(讀取、賦值等)都必須在工作內(nèi)存中進行,而不能直接讀寫主內(nèi)存中的數(shù)據(jù)。不同的線程之間也無法直接訪問對方工作內(nèi)存中的變量,線程間變量值的傳遞均需要通過主內(nèi)存來完成。
這里所講的主內(nèi)存、工作內(nèi)存與Java內(nèi)存區(qū)域中的Java堆、棧、方法區(qū)等并不是同一個層次的對內(nèi)存的劃分,這兩者基本上是沒有任何關(guān)系的。如果兩者一定要勉強對應(yīng)起來,那么從變量、主內(nèi)存、工作內(nèi)存的定義來看,主內(nèi)存主要對應(yīng)于Java堆中的對象實例數(shù)據(jù)部分,而工作內(nèi)存則對應(yīng)于虛擬機棧中的部分區(qū)域。
從更基礎(chǔ)的層次上說,主內(nèi)存直接對應(yīng)于物理硬件的內(nèi)存,而為了獲取更好的運行速度,虛擬機(或者是硬件、操作系統(tǒng)本身的優(yōu)化措施)可能會讓工作內(nèi)存優(yōu)先存儲于寄存器和高速緩存中,因為程序運行時主要訪問的是工作內(nèi)存。
1.2 內(nèi)存間的交互
關(guān)于主內(nèi)存與工作內(nèi)存之間具體的交互協(xié)議,即一個變量如何從主內(nèi)存拷貝到工作內(nèi)存、如何從工作內(nèi)存同步回主內(nèi)存這一類的實現(xiàn)細節(jié),Java內(nèi)存模型中定義了以下8種操作來完成。Java虛擬機實現(xiàn)時必須保證下面提及的每一種操作都是原子的、不可再分的(對于double和long類型的變量來說,load、store、read和write操作在某些平臺上允許有例外)。
- lock(鎖定):作用于主內(nèi)存的變量,它把一個變量標識為一條線程獨占的狀態(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í)行引擎,每當虛擬機遇到一個需要使用變量的值的字節(jié)碼指令時將會執(zhí)行這個操作。
- assign(賦值):作用于工作內(nèi)存的變量,它把一個從執(zhí)行引擎接收的值賦給工作內(nèi)存的變量,每當虛擬機遇到一個給變量賦值的字節(jié)碼指令時執(zhí)行這個操作。
- store(存儲):作用于工作內(nèi)存的變量,它把工作內(nèi)存中一個變量的值傳送到主內(nèi)存中,以便隨后的write操作使用。
- write(寫入):作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中。
非原子性協(xié)定:
Java內(nèi)存模型要求lock、unlock、read、load、assign、use、store、write這八種操作都具有原子性,但是對于64位的數(shù)據(jù)類型(long和double),在模型中特別定義了一條寬松的規(guī)定:允許虛擬機將沒有被volatile修飾的64位數(shù)據(jù)的讀寫操作劃分為兩次32位的操作來進行,即允許虛擬機實現(xiàn)自行選擇是否要保證64位數(shù)據(jù)類型的load、store、read和write這四個操作的原子性,這就是所謂的“long和double的非原子性協(xié)定”(Non-Atomic Treatment of double and long Variables)。
如果要把一個變量從主內(nèi)存拷貝到工作內(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進行訪問時,一種可能出現(xiàn)的順序是read a、read b、load b、load a。
Java內(nèi)存模型還規(guī)定了在執(zhí)行上述8種基本操作時必須滿足如下規(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)的變量,換句話說就是對一個變量實施use、store操作之前,必須先執(zhí)行assign和load操作。
- 一個變量在同一個時刻只允許一條線程對其進行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操作)。
2. 關(guān)于 Volatile 變量
Volatile變量具備的三個關(guān)鍵點(保證可見性,不能保證原子性,禁止指令重排序):
- 用volatile聲明一個變量可以保證對所有線程的可見性,這里的“可見性”是指當一條線程修改了這個變量的值,新值對于其他線程來說是可以及時得知的(并不是立即可見的,從物理存儲的角度看,各個線程的工作內(nèi)存中volatile變量也可以存在不一致的情況,但由于每次使用之前都要先刷新,執(zhí)行引擎看不到不一致的情況,因此可以認為不存在一致性問題))。
- 基于volatile變量的運算在并發(fā)下并不是線程安全的。
- 禁止指令重排序優(yōu)化,普通的變量僅會保證在該方法的執(zhí)行過程中所有依賴賦值結(jié)果的地方都能獲取到正確的結(jié)果,而不能保證變量賦值操作的順序與程序代碼中的執(zhí)行順序一致。因為在同一個線程的方法執(zhí)行過程中無法感知到這點,這就是Java內(nèi)存模型中描述的所謂“線程內(nèi)表現(xiàn)為串行的語義”(As-If-Serial)。
對于 As-If-Serial 的簡要說明:對于處理器或者編譯器來說,在進行指令重排序優(yōu)化(為了提高并行度)的時候只能保證在單線程環(huán)境下的串行化語義的一致性。
舉個栗子,下面一個雙鎖檢測(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(); } }
對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í)行了一個 lock addl$0x0,(%esp)
操作,這個操作的作用相當于一個內(nèi)存屏障 (Memory Barrier或Memory Fence,指重排序時不能把后面的指令重排序到內(nèi)存屏障之前的位置),只有一個處理器訪問內(nèi)存時,并不需要內(nèi)存屏障;但如果有兩個或更多處理器訪問同一塊內(nèi)存,且其中有一個在觀測另一個,就需要內(nèi)存屏障來保證一致性了。
這句指令中的 addl$0x0,(%esp)
(把ESP寄存器的值加0)顯然是一個空操作,之所以用這個空操作而不是空操作專用指令nop,是因為IA32手冊規(guī)定lock前綴不允許配合nop指令使用。這里的關(guān)鍵在于lock前綴,查詢IA32手冊可知,它的作用是將本處理器的緩存寫入了內(nèi)存,該寫入動作也會引起別的處理器或者別的內(nèi)核無效化(Invalidate)其緩存,這種操作相當于對緩存中的變量做了一次前面介紹Java內(nèi)存模式中所說的“store和write”操作。所以通過這樣一個空操作,可讓前面volatile變量的修改對其他處理器立即可見。
那為何說它禁止指令重排序呢?從硬件架構(gòu)上講,指令重排序是指處理器采用了允許將多條指令不按程序規(guī)定的順序分開發(fā)送給各個相應(yīng)的電路單元進行處理。但并不是說指令任意重排,處理器必須能正確處理指令依賴情況保障程序能得出正確的執(zhí)行結(jié)果。譬如指令1把地址A中的值加10,指令2把地址A中的值乘以2,指令3把地址B中的值減去3,這時指令1和指令2是有依賴的,它們之間的順序不能重排—— (A+10)*2
與 A*2+10
顯然不相等,但指令3可以重排到指令1、2之前或者中間,只要保證處理器執(zhí)行后面依賴到A、B值的操作時能獲取正確的A和B值即可。所以在同一個處理器中,重排序過的代碼看起來依然是有序的。因此,lock addl$0x0,(%esp)
指令把修改同步到內(nèi)存時,意味著所有之前的操作都已經(jīng)執(zhí)行完成,這樣便形成了“指令重排序無法越過內(nèi)存屏障”的效果。
假定T表示一個線程,V和W分別表示兩個volatile型變量,那么在進行read、load、use、assign、store和write操作時需要滿足如下對于volatile變量的特殊規(guī)則:
- 只有當線程T對變量V執(zhí)行的前一個動作是load的時候,線程T才能對變量V執(zhí)行use動作;并且,只有當線程T對變量V執(zhí)行的后一個動作是use的時候,線程T才能對變量V執(zhí)行l(wèi)oad動作。線程T對變量V的use動作可以認為是和線程T對變量V的load、read動作相關(guān)聯(lián)的,必須連續(xù)且一起出現(xiàn)。(及時得到volatile變量的新值)
- 只有當線程T對變量V執(zhí)行的前一個動作是assign的時候,線程T才能對變量V執(zhí)行store動作;并且,只有當線程T對變量V執(zhí)行的后一個動作是store的時候,線程T才能對變量V執(zhí)行assign動作。線程T對變量V的assign動作可以認為是和線程T對變量V的store、write動作相關(guān)聯(lián)的,必須連續(xù)且一起出現(xiàn)。(及時將volatile變量的改動同步到主存)
- 假定動作A是線程T對變量V實施的use或assign動作,假定動作F是和動作A相關(guān)聯(lián)的load或store動作,假定動作P是和動作F相應(yīng)的對變量V的read或write動作;與此類似,假定動作B是線程T對變量W實施的use或assign動作,假定動作G是和動作B相關(guān)聯(lián)的load或store動作,假定動作Q是和動作G相應(yīng)的對變量W的read或write動作。如果A先于B,那么P先于Q。(禁止指令重排序優(yōu)化)
3. 關(guān)于內(nèi)存屏障
內(nèi)存屏障又稱內(nèi)存柵欄(Memory Barrier)是一個CPU指令,它的作用有兩個:
- 保證特定操作的執(zhí)行順序
- 保證某些變量的內(nèi)存可見性(利用該特性實現(xiàn)volatile的內(nèi)存可見性)
由于編譯器和處理器都能執(zhí)行指令重排優(yōu)化。如果在指令間插入一條Memory Barrier則會告訴編譯器和CPU,不管什么指令都不能和這條Memory Barrier指令重排序,也就是說通過插入內(nèi)存屏障禁止在內(nèi)存屏障前后的指令執(zhí)行重排序優(yōu)化。Memory Barrier的另外一個作用是強制刷出各種CPU的緩存數(shù)據(jù),因此任何CPU。上的線程都能讀取到這些數(shù)據(jù)的最新版本??傊畍olatile變量正是通過內(nèi)存屏障實現(xiàn)其在內(nèi)存中的語義,即可見性和禁止重排優(yōu)化。
Intel硬件提供了一系列的內(nèi)存屏障, 主要有:
- Ifence,是一種Load Barrier讀屏障
- sfence,是一種Store Barrier寫屏障
- mfence,是一種全能型的屏障, 具備ifence和sfence的能力
- Lock前綴, Lock不是一種內(nèi)存屏障,但是它能完成類似內(nèi)存屏障的功能。Lock會對CPU總線和高速緩存加鎖,可以理解為CPU指令級的一種鎖。它后面可以跟ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG等指令。
不同硬件實現(xiàn)內(nèi)存屏障的方式不同,Java內(nèi)存模型屏蔽了這種底層硬件平臺的差異,由JVM來為不同的平臺生成相應(yīng)的機器碼。
JVM中提供了四類內(nèi)存屏障指令:
volatile內(nèi)存語義的實現(xiàn):
總的來說:
- 當?shù)谝粋€操作是volatile讀時,不管第二個操作是什么,都不能重排序。這個規(guī)則確保volatile讀之后的操作不會被編譯器重排序到volatile讀之前。
- 當?shù)诙€操作是volatile寫時,不管第一個操作是什么, 都不能重排序。這個規(guī)則確保volatile寫之前的操作不會被編譯器重排序到volatile寫之后。
- 當?shù)谝粋€操作是volatile寫,第二個操作是volatile讀或?qū)憰r,不能重排序。
為了實現(xiàn)volatile的內(nèi)存語義,編譯器在生成字節(jié)碼時,會在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序。對于編譯器來說,發(fā)現(xiàn)一個最優(yōu)布置來最小化插入屏障的總數(shù)幾乎不可能。為此, JMM采取保守策略
下面是基于保守策略的JMM內(nèi)存屏障插入策略:
- 在每個volatile寫操作的前面插入一個StoreStore屏障。
- 在每個volatile寫操作的后面插入一個StoreLoad屏障。
- 在每個volatile讀操作的后面插入一個LoadLoad屏障。
- 在每個volatile讀操作的后面插入一個LoadStore屏障。
上述內(nèi)存屏障插入策略非常保守,但它可以保證在任意處理器平臺,任意的程序中都能得到正確的volatile內(nèi)存語義。
上圖中StoreStore屏障可以保證在volatile寫之前,其前面的所有普通寫操作已經(jīng)對任意處理器可見了。這是因為StoreStore屏障將保障 上面所有的普通寫在volatile寫之前刷新到主內(nèi)存。
上圖中StoreLoad屏障的作用是避免volatile寫與后面可能有的 volatile讀/寫 操作重排序。因為編譯器常常無法準確判斷在一個volatile寫的后面 否需要插入一個StoreLoad屏障(比如,一個volatile寫之后方法立即return)。為了保證能正確實現(xiàn)volatile的內(nèi)存語義,JMM在采取了保守策略:在每個volatile寫的后面,或者在每個volatile讀的前面插入一個StoreLoad屏障。
從整體執(zhí)行效率的角度考慮,JMM最終選擇了在每個volatile寫的后面插入一個StoreLoad屏障。因為volatile寫-讀內(nèi)存語義的常見使用模式是:一個寫線程寫volatile變量,多個讀線程讀同一個volatile變量。 當讀線程的數(shù)量大大超過寫線程時,選擇在volatile寫之后插入StoreLoad屏障將帶來可觀的執(zhí)行效率的提升。從這里可以看到JMM在實現(xiàn)上的一個特點:首先確保正確性,然后再去追求執(zhí)行效率。
上圖中LoadLoad屏障用來禁止處理器把上面的volatile讀與下面的普通讀重排序。LoadStore屏障用來禁 止處理器把上面的volatile讀與下面的普通寫重排序。
class VolatileBarrierExample { int a; volatile int v1 = 1; volatile int v2 = 2; void readAndwrite() { inti = v1; //第一個volatile讀 intj = v2 //第二個volatile讀 a = i+j; // 普通寫 v1 = i+ 1; //第一個volatile寫 v2=j * 2; //第二個volatile寫 } }
針對readAndWrite()方法,編譯器在生成字節(jié)碼時可以做如下的優(yōu)化。
注意,最后的StoreLoad屏障不能省略。因為第二個volatile寫之后, 方法立即return。 此時編譯器可能無法準確斷定后面是否會有volatile讀或?qū)?,為了安全起見編譯器通常會在這里插入一個StoreLoad屏障。上面的優(yōu)化針對任意處理器平臺,由于不同的處理器有不同“松緊度"的處理器內(nèi)存模型,內(nèi)存屏障的插入還可以根據(jù)具體的處理器內(nèi)存模型繼續(xù)優(yōu)化。
4. 原子性、可見性與有序性
保證原子性(Atomicity):
- JMM定義的原子操作:JMM來直接保證的原子性變量的操作包括read、load、assign、use、store和write這六個。
- synchronized關(guān)鍵字:Java內(nèi)存模型還提供了lock和unlock操作來滿足更大范圍的原子性保證,盡管虛擬機未把lock和unlock操作直接開放給用戶使用,但是卻提供了更高層次的字節(jié)碼指令monitorenter和monitorexit來隱式地使用這兩個操作。反映到Java代碼中就是同步塊synchronized關(guān)鍵字,因此在synchronized塊之間的操作也具備原子性。
保證可見性(Visibility):
- volatile關(guān)鍵字:JMM對于volatile定義的特殊規(guī)則保證了新值能立即同步到主內(nèi)存,以及每次使用前立即從主內(nèi)存刷新來保證可見性的。
- synchronized關(guān)鍵字:同步塊的可見性是由“對一個變量執(zhí)行unlock操作之前,必須先把此變量同步回主內(nèi)存中(執(zhí)行store、write操作)”這條規(guī)則獲得的。
- final關(guān)鍵字:被final修飾的字段在構(gòu)造器中一旦被初始化完成,并且構(gòu)造器沒有把“this”的引用傳遞出去(this引用逃逸是一件很危險的事情,其他線程有可能通過這個引用訪問到“初始化了一半”的對象),那么在其他線程中就能看見final字段的值。
保證有序性(Ordering):
- As-If-Serial:如果在本線程內(nèi)觀察,所有的操作都是有序的。
- volatile關(guān)鍵字:volatile關(guān)鍵字本身就包含了禁止指令重排序的語義,在具體的實現(xiàn)中(依賴內(nèi)存屏障)就會保證指令的有序性。
- synchronized關(guān)鍵字:“一個變量在同一個時刻只允許一條線程對其進行l(wèi)ock操作”這條規(guī)則決定了持有同一個鎖的兩個同步塊只能串行地進入而保證執(zhí)行時的有序性。
5. Happens-Before
先行發(fā)生是(Happens-Before) Java內(nèi)存模型中定義的兩項操作之間的偏序關(guān)系,比如說操作A先行發(fā)生于操作B,其實就是說在發(fā)生操作B之前,操作A產(chǎn)生的影響能被操作B觀察到,“影響”包括修改了內(nèi)存中共享變量的值、發(fā)送了消息、調(diào)用了方法等。(先行發(fā)生原則是JMM的實現(xiàn)所體現(xiàn)出的一些特定的現(xiàn)象)Java語言無須任何同步手段保障就能成立的先行發(fā)生規(guī)則有且只有下面這些:
- 程序次序規(guī)則(Program Order Rule):在一個線程內(nèi),按照控制流順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作。注意,這里說的是控制流順序而不是程序代碼順序,因為要考慮分支、循環(huán)等結(jié)構(gòu)。
- 管程鎖定規(guī)則(Monitor Lock Rule):一個unlock操作先行發(fā)生于后面對同一個鎖的lock操作。這里必須強調(diào)的是“同一個鎖”,而“后面”是指時間上的先后。
- volatile變量規(guī)則(Volatile Variable Rule):對一個volatile變量的寫操作先行發(fā)生于后面對這個變量的讀操作,這里的“后面”同樣是指時間上的先后。
- 線程啟動規(guī)則(Thread Start Rule):Thread對象的start()方法先行發(fā)生于此線程的每一個動作。
- 線程終止規(guī)則(Thread Termination Rule):線程中的所有操作都先行發(fā)生于對此線程的終止檢測,我們可以通過Thread::join()方法是否結(jié)束、Thread::isAlive()的返回值等手段檢測線程是否已經(jīng)終止執(zhí)行。
- 線程中斷規(guī)則(Thread Interruption Rule):對線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生,可以通過Thread::interrupted()方法檢測到是否有中斷發(fā)生。
- 對象終結(jié)規(guī)則(Finalizer Rule):一個對象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)先行發(fā)生于它的finalize()方法的開始。
- 傳遞性(Transitivity):如果操作A先行發(fā)生于操作B,操作B先行發(fā)生于操作C,那就可以得出操作A先行發(fā)生于操作C的結(jié)論。
舉個栗子:
private int value = 0; pubilc void setValue(int value){ this.value = value; } public int getValue(){ return value; }
假設(shè)存在線程A和B,線程A先(時間上的先后)調(diào)用了setValue(1),然后線程B調(diào)用了同一個對象的getValue(),那么線程B收到的返回值是什么?
根據(jù)先行發(fā)生原則中的各項規(guī)則來進行判斷:
- 由于兩個方法分別由線程A和B調(diào)用,不在一個線程中,所以程序次序規(guī)則在這里不適用;
- 由于沒有同步塊,自然就不會發(fā)生lock和unlock操作,所以管程鎖定規(guī)則不適用;
- 由于value變量沒有被volatile關(guān)鍵字修飾,所以volatile變量規(guī)則不適用;
- 后面的線程啟動、終止、中斷規(guī)則和對象終結(jié)規(guī)則也和這里完全沒有關(guān)系;
- 因為沒有一個適用的先行發(fā)生規(guī)則,所以最后一條傳遞性也無法滿足;
因此我們可以判定,盡管線程A在操作時間上先于線程B,但是無法確定線程B中g(shù)etValue()方法的返回結(jié)果,換句話說,這里面的操作不是線程安全的。
再舉個栗子:
// 以下操作在同一個線程中執(zhí)行 int i = 1; int j = 2;
根據(jù)程序次序規(guī)則,“int i=1”的操作先行發(fā)生于“int j=2”,但是“int j=2”的代碼完全可能先被處理器執(zhí)行,這并不影響先行發(fā)生原則的正確性,因為我們在這條線程之中沒有辦法感知到這一點。
到此這篇關(guān)于必須要學會的JMM與volatile的文章就介紹到這了,更多相關(guān)JMM與volatile內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot整合RocketMQ批量發(fā)送消息的實現(xiàn)代碼
這篇文章主要介紹了SpringBoot整合RocketMQ批量發(fā)送消息的實現(xiàn),文中通過代碼示例講解的非常詳細,對大家的學習或工作有一定的幫助,需要的朋友可以參考下2024-04-04一篇文章帶了解如何用SpringBoot在RequestBody中優(yōu)雅的使用枚舉參數(shù)
這篇文章主要介紹了SpringBoot中RequestBodyAdvice使用枚舉參數(shù),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-08-08struts2+spring+ibatis框架整合實現(xiàn)增刪改查
這篇文章主要為大家詳細介紹了struts2+spring+ibatis框架整合實現(xiàn)增刪改查操作,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-07-07SpringBoot整合SpringBoot-Admin實現(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方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-09-09