Java内存模型、volatile、原子性、可见性、有序性、happens-before原则
目錄
1.硬件的效率與一致性:
緩存一致性(Cache Coherence)
2.Java內(nèi)存模型
2.1主內(nèi)存與工作內(nèi)存
2.2內(nèi)存間的交互
2.3 volatile型變量的特殊規(guī)則
2.3.1 保證此變量對所有線程的可見性;
2.3.2 禁止指令重排序優(yōu)化
2.3.4 在volatile與鎖之中選擇的唯一依據(jù)
2.3.4 JMM中對volatile變量定義的特殊規(guī)則
2.4對于double和long型變量的特殊規(guī)則
2.5 原子性、可見性和有序性
2.5.1原子性atomicity
2.5.2 可見性visibility
2.5.3 有序性 ordering
2.6 先行發(fā)生原則happens-before
JMM的天然的先行發(fā)生關(guān)系
并發(fā)處理的廣泛應用是使得Amdahl定律代替摩爾定律成為計算機性能發(fā)展原動力的根本原因。
多任務處理:讓計算機同時去做幾件事情。
- 很重要的原因:計算機的運算速度與它的存儲和通信子系統(tǒng)虛度差距太大,大量的時間都花費在磁盤I/O、網(wǎng)絡通信或數(shù)據(jù)庫訪問上。
每秒事務處理數(shù)(transacrions per seconds,TPS):衡量一個服務性能的高低好壞。
- 它代表著一秒內(nèi)服務端平均能響應的請求總數(shù)
- 與并發(fā)能力有很大關(guān)系:
- 計算量相同的任務,程序線程并發(fā)協(xié)調(diào)得有條不紊,效率自然會高;
- 線程之間頻繁發(fā)生阻塞甚至死鎖,將會大大降低程序的并發(fā)能力。
1. 硬件的效率與一致性:
物理計算機中的并發(fā):
- 執(zhí)行任務:處理器至少要完成與內(nèi)存交互,如讀取數(shù)據(jù)、存儲運算結(jié)果等。
- 這個I/O操作很難消除。
- 計算機的存儲設備與處理器的運算速度由幾個數(shù)量及的差距,所以現(xiàn)代計算機系統(tǒng)都加入了一層讀寫速度盡可能接近處理器運算速度的高速緩存(cache)來作為內(nèi)存與處理器之間的緩沖;
- 將運算需要使用到的數(shù)據(jù)復制到緩存中,讓運算能快速進行;
- 當運算結(jié)束后,再從緩存同步到內(nèi)存中,
- 這樣處理器就無需等待緩慢的內(nèi)存讀寫了。
基于高速緩存的存儲交互很好地解決了處理器與內(nèi)存的速度矛盾,但也引入了一個新問題:
緩存一致性(Cache Coherence)
百度百科:又譯為緩存連貫性、緩存同調(diào),是指保留在高速緩存中的共享資源,保持數(shù)據(jù)一致性的機制。
在多處理器系統(tǒng)中,每個處理器都有自己的高速緩存,而它們又共享同一主內(nèi)存(Main Memory),如圖所示:
當多個處理器的運算任務都涉及同一塊主內(nèi)存區(qū)域時,將可能導致各自的緩存數(shù)據(jù)不一致。
這就需要各個處理器訪問緩存時都遵循一些協(xié)議:MSI、MESI、MOSI、Synapse、Firefly及Dragon Protocol等。
內(nèi)存模型:在特定操作協(xié)議下,對特定的內(nèi)存或者告訴緩存進行讀寫訪問的過程抽象。
- 不同架構(gòu)可以擁有不一樣的內(nèi)存模型
- JVM也有自己的內(nèi)存模型。
除了增加高速緩存以外,為了使得處理器內(nèi)部的運算單元盡量被充分利用,處理器可能會對輸入代碼進行亂序執(zhí)行(out-of-order execution)優(yōu)化,處理器會在計算之后將亂序的執(zhí)行結(jié)果重組,保證該結(jié)果與順序執(zhí)行的結(jié)果是一致的,但并不保證程序各個語句計算的先后順序與輸入代碼中的順序一致。
所以——如果存在一個計算任務依賴另外一個計算任務的中間結(jié)果,那么其順序性并不能靠代碼的先后順序保證。
JVM的即時編譯器中也有和亂序執(zhí)行優(yōu)化類似的指令重排序(instruction reorder)優(yōu)化。
2. Java內(nèi)存模型
JMM,來屏蔽各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,以實現(xiàn)讓Java程序在各個平臺下都能達到一致的內(nèi)存訪問效果。
2.1 主內(nèi)存與工作內(nèi)存
JMM的主要目標是定義程序中各個變量的訪問規(guī)則,即在虛擬機中將變量存儲到內(nèi)存和從內(nèi)存中取出變量這樣的底層細節(jié)。
- 變量(variables):包括實例字段、靜態(tài)字段和構(gòu)成數(shù)組對象的元素,但不包括局部變量與方法參數(shù)。
- 因為后面的是線程私有的,不會被共享。
為了獲得較高的執(zhí)行效能,JMM并沒有限制執(zhí)行引擎適用處理器的特定寄存器或緩存來和主內(nèi)存進行交互,也沒有限制即時編譯器進行調(diào)整代碼執(zhí)行順序這類優(yōu)化措施。
JMM規(guī)定,所有的變量都存儲在主內(nèi)存(Main memory)(可類比,物理硬件是的主內(nèi)存,虛擬機內(nèi)存中的一部分)。
每條線程有自己的工作內(nèi)存(working memory)(可類比。處理器高速緩存)。
- 線程的工作內(nèi)存保存了被該線程使用到的變量的主內(nèi)存副本拷貝。
- 線程對變量的所有操作(讀取、賦值等)必須在工作內(nèi)存中進行,而不能把自己接讀寫主內(nèi)存中的變量。
- 不同線程之間也無法直接訪問對方的工作內(nèi)存中的變量。
- 線程間變量值的傳遞均需要通過主內(nèi)存來完成。
線程、主內(nèi)存、工作內(nèi)存三者之間的交互關(guān)系如圖:
這里所提到的主內(nèi)存、工作內(nèi)存與Java內(nèi)存區(qū)域中的Java堆、棧、方法區(qū)等并不是同一個層次的內(nèi)存劃分。基本上沒有聯(lián)系。
2.2 內(nèi)存間的交互
JMM中定義了一下8種操作來完成主內(nèi)存與工作內(nèi)存之間具體的交互協(xié)議(即一個變量如何從主內(nèi)存拷貝到工作內(nèi)存,如何從工作內(nèi)存同步會主內(nèi)存之類的實現(xiàn)細節(jié)),虛擬機必須保證下面提及的每一種操作都是原子的、不可再分的(對于double和long類型的變量來說,load、store、read和write操作在某些平臺商允許有例外):
除此之外,JMM還規(guī)定了在執(zhí)行者8種基本操作必須滿足如下規(guī)則:
2.3 volatile型變量的特殊規(guī)則
關(guān)鍵字volatile是JVM提供的最輕量級的同步機制。
當一個變量定義為volatile之后,它將具備兩種特性:
2.3.1 保證此變量對所有線程的可見性
- 可見性:指當一條線程修改了這個變量的值,新值對于其他線程來說是可以立即得知的
- 而普通變量的值在線程間傳遞需要通過主內(nèi)存來完成。
- volatile變量對所有線程是立即可見的,對volatile變量的所有的寫操作都能立即反映到其它線程之中,即volatile變量在各個線程中是一致的。
- 但是并不能得出“基于volatile變量的運算在并發(fā)下是安全的”。
- volatile變量在各個線程的工作內(nèi)存中不存在一致性問題(在各個線程的工作內(nèi)存中,volatile變量也可以存在不一致的情況,但由于每次使用之前都需要刷新,執(zhí)行引擎可以看到不一致的情況,因此可以認為不存在一致性問題)。
- 但在Java里面的運算并非原子操作,導致volatile變量的運算在并發(fā)下一樣是不安全的。如下代碼演示了可說明原因:
運行結(jié)果:
這段代碼發(fā)起了20個線程,每個線程對race變量進行1000次自增操作。
如果這段代碼能夠正確并發(fā)的話,最后輸出的結(jié)果應該是200000.
但運行后并不會得到期望的結(jié)果,總小于200000.
問題:自增運算race++
使用Javap反編譯這段代碼后,得到:
public static void increase();Code:Stack=2,Locals=0,Args_size=00: getstatic #13;//Field race:I3: iconst_14: iadd5:putstatic #13;//Field race:I8: return LineNumberTable:line 14:0line 15:8只有一行代碼的increase()方法在Class文件中是由4字節(jié)碼指令構(gòu)成的(return 不是有race++產(chǎn)生的,這條指令可以不計算),從字節(jié)碼層面上很容易就分析出并發(fā)失敗的原因了:
- 當getstatic指令把race的值取到操作棧頂時,volatile關(guān)鍵字保證了race的值此時是正確的;
- 但是在執(zhí)行iconst_1、iadd這些指令的時候,其它線程可能已經(jīng)把race的值加大了,而在操作棧頂?shù)闹稻妥兂闪诉^起的數(shù)據(jù),所以putstatic指令執(zhí)行后就可能把較小的race值同步回主內(nèi)存中。
即使編譯出來只有一條字節(jié)碼指令,也并不意味著執(zhí)行這條指令就是一個原子操作。
- 一條字節(jié)碼指令在解釋執(zhí)行時,解釋器將要運行許多行代碼才能實現(xiàn)它的語義,如果是編譯執(zhí)行,一條字節(jié)碼指令也可能轉(zhuǎn)化為若干條本地機器指令,此處使用-XX:+PrintAssembly參數(shù)輸出反匯編來分析會更嚴謹些。
由于volatile變量只能保證可見性,在不符合以下兩條規(guī)則的運算場景中,仍需要通過加鎖(使用synchronized或java.util.concurrent中的原子類)來保證原子性:
某個變量定義為volatile的應用場景:
- 運算結(jié)果并不依賴變量的當前值,或者能夠確保自由單一的線程修改變量的值;
- 變量不需要與其他的狀態(tài)變量共同參與不變約束。
很適合使用volatile變量來控制并發(fā)的場景:
volatile boolean shutdownRequested; public void shutdown() {shutdownRequested=true; } public void dowork() {while(!shutdownRequested) {//do stuff} }當shutdown()方法被調(diào)用時,能保證所有線程中執(zhí)行的dowork()方法都立即停下來。
2.3.2 禁止指令重排序優(yōu)化
普通的變量僅僅會保證在該方法的執(zhí)行過程中所有依賴賦值結(jié)果的地方都能獲取到正確的結(jié)果,而不能保證變量賦值操作的順序與程序代碼中的執(zhí)行順序一致。——線程內(nèi)表現(xiàn)為串行的語義(within-thread as-if-serial semantics)。
如下例子展示了為何指令重排序會干擾并發(fā)的執(zhí)行:
Map configOptions; char[] configText; //此變量必須定義為volatile volatile boolean initialized=false;//假設以下代碼在線程A中執(zhí)行 //模擬讀取配置信息,當讀取完成后將initialized設置為true以通知其它線程配置可用 configOptions=new HashMap(); configTest=readConfigFile(fileName); processConfigOptions(configText,configOptions); initialized=true;//假設以下代碼在線程B中執(zhí)行 //等待initialized為true,代表線程A已經(jīng)把配置信息初始化完成; while(!initialized) {sleep(); } //使用線程A中初始化好的配置信息 doSomethingWithConfig();如果initialized變量沒有用volatile修飾,就可能會由于指令重排序的優(yōu)化,導致位于線程A的最后一句代碼initialized=true被提前執(zhí)行(這里雖然使用Java作為偽代碼,但所指的重排序優(yōu)化是機器及的優(yōu)化操作,提前執(zhí)行是指這句話對應的匯編代碼被提前執(zhí)行),這樣,在線程B中使用配置信息的代碼就可能出現(xiàn)錯誤,而volatile關(guān)鍵字則可以避免此類情況的發(fā)生。
一下代碼分析了volatile關(guān)鍵字是如何禁止指令重排序優(yōu)化的:
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)鍵變化在于由volatile修飾前面mov%eax,0x150(%esi)這句便是賦值操作)多執(zhí)行了一個“l(fā)ock addl $ 0x0, (%esp)"操作,這個操作相當于一個內(nèi)存屏障(memory barrier或memory fence,指重排序是不能把后面的指令重排序到內(nèi)存屏障之前的位置),只有一個CPU訪問內(nèi)存時,并不需要內(nèi)存屏障;
但如果有兩個或更多CPU訪問同一塊內(nèi)存,且其中有一個在觀測另一個,就需要內(nèi)存屏障來保證一致性了。
addl $ 0x0, (%esp)這句指令,把ESP寄存器的值加0,顯然是一個空操作。采用這個空操作而不是空操作指令nop是因為IA32手冊規(guī)定lock前綴不允許配合nop指令使用。
關(guān)鍵在于Lock前綴。它的作用是使得本CPU的Cache寫入了內(nèi)存,該寫入動作也會引起別的CPU或者別的內(nèi)核無效化(Invalidae)其cache,這種操作相當于對cache中的變量做了一次前面介紹的JMM中所說的“store和write”操作。所以通過這樣一個空操作,可以讓前面的volatile變量的修改對其他CPU立即可見。
volatile禁止重排序:
從硬件架構(gòu)上講,指令重排序是指CPU采用了允許將多條指令不按程序規(guī)定的順序分開發(fā)送給各個響應的電路單元處理。但并不是說,指令任意重排,CPU需要能正確處理指令以來情況以保障程序能得到正確的執(zhí)行結(jié)果。
所以在本CPU內(nèi),重排序看起來依然是有序的。因此lock?addl $ 0x0, (%esp)指令把修改同步到內(nèi)存中,意味著之前所欲的操作都已經(jīng)執(zhí)行完成,這樣便形成了“指令重排序無法越過內(nèi)存屏障”的效果。
volatile變量讀操作的性能消耗與普通變量幾乎沒有什么差別,但是寫操作可能會慢點,因為他需要在本地代碼中插入許多內(nèi)存屏障指令來保證處理器不發(fā)生亂序執(zhí)行。
2.3.4 在volatile與鎖之中選擇的唯一依據(jù)
僅僅是volatile的語義能否滿足使用場景的需求。
2.3.4 JMM中對volatile變量定義的特殊規(guī)則
① 在工作內(nèi)存中每次使用變量前都需要從主內(nèi)存中刷新最新值;
② 每次修改變量的值之后都必須立刻同步到主內(nèi)存中;
③ 要求volatile修飾的變量不會被指令重新排序。
假定T表示一個線程,V和W分別表示兩個volatile型變量。那么在進行read、load、use、assign、store和write操作時需要滿足一下規(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)(這條規(guī)則要求在工作內(nèi)存中,每次使用V前都必須現(xiàn)充主內(nèi)存刷新最新的值,用于保證能看見其它線程對V所做的修改后的值)。
- 只有當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ù)一起粗線(這條規(guī)則要求在工作內(nèi)存中,每次修改V后都必須立即同步回主內(nèi)存中,用于保證其他線程可以看到自己對V所做的修改)。
- 假定動作A是T對V實施的use或assign動作,假定動作F是和動作A相關(guān)聯(lián)的load或store動作,假定動作P是和動作F相對應的對V的read或write動作;類似地,假定動作B是對變量W實施的use或assign動作,假定動作G是和動作B相關(guān)聯(lián)的load或store動作,假定動作Q是和動作G相對應的對W的read或write動作。如果A先于B,那么P先于Q(這條規(guī)則要求volatile修飾的變量不會被指令重排序優(yōu)化,保證代碼的執(zhí)行順序與程序的順序相同)。
2.4 對于double和long型變量的特殊規(guī)則
JMM要求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、read、store和write這4個操作的原子性。——long和double的非原子性協(xié)定(Nonatomic treament of double and long variables)。
如果有多個線程共享一個并未聲明為volatile的long或double類型的變量,并且同時對他們進行讀取和修改操作,那么某些線程可能會讀取到一個既非原值,也不是其它線程修改值的代表了“半個變量”的數(shù)值(很罕見)。
- 因為,JMM雖然允許虛擬機不把long和double變量的讀寫實現(xiàn)為原子操作,但允許虛擬機選擇把這些操作時限為具有原子性的操作,而且還“強烈建議”虛擬機這樣實現(xiàn)。
- 因此,在編寫代碼的時候一般不需要把用到的long和double變量專門聲明為volatile。
2.5 原子性、可見性和有序性
2.5.1 原子性atomicity
由JMM來直接保證的原子性變量操作包括:
- read
- load
- assign
- use
- store
- write
可以大致認為基本數(shù)據(jù)類型的訪問和讀寫是具備原子性的(例外就是long和double的非原子性協(xié)定)。
如果應用場景中需要更大范圍的原子性保證(經(jīng)常會遇到),JMM還提供了lock和unlock操作來滿足這種需求。
盡管虛擬機未把lock和unclock操作直接開放給用戶使用,但是卻提供了更高層次的字節(jié)碼指令monitoerenter和monitorexit來隱式地使用這兩個操作,這兩個字節(jié)碼指令反應在Java代碼中就是同步塊——synchronized關(guān)鍵字,因此在synchronized塊之間的操作也具備原子性。
2.5.2 可見性visibility
指當一個線程修改了共享變量的值,其它線程能夠立即得知這個修改。
JMM是通過在變量修改后將新值同步回主內(nèi)存中,在變量讀取前從主內(nèi)存刷新變量值這種依賴主內(nèi)存作為傳遞媒介的方式來實現(xiàn)可見性的。
無論是普通變量還是volatile變量都是如此。
普通變量和volatile的區(qū)別是:
- volatile的特殊規(guī)則保證了新值能立即同步到主內(nèi)存,以及每次使用前立即從主內(nèi)存刷新。
- 因此,volatile保證了多線程操作是變量的可見性
- 而普通變量則不能保證這一點。
Java還有synchronized和final關(guān)鍵字可以實現(xiàn)可見性。
- 同步塊的可見性是由“對一個變量執(zhí)行unlock之前,必須先把此變量同步回主內(nèi)存中(執(zhí)行store、write操作)”這條規(guī)則獲得的。
- final關(guān)鍵字的可見性:被final修飾的字段在構(gòu)造其中一旦初始化完成,并且構(gòu)造器沒有把this的引用傳遞出去(this引用逃逸是一件很危險的事,其它線程可能通過這個引用訪問到“初始化了一半”的對象),那在其它線程中就能看見final字段的值。如下代碼:
2.5.3 有序性 ordering
Java程序的天然有序性可以總結(jié)為一句話:如果在本線程內(nèi)觀察,所有的操作都是有序的;如果在一個線程中觀察另一個線程,所有的操作都是無序的。
- 前半句:線程內(nèi)表現(xiàn)為串行語義——within-thread as-if-serial semantics
- 后半句:指令重排序現(xiàn)象和工作內(nèi)存與主內(nèi)存同步延遲現(xiàn)象
Java語言提供了volatile和synchronized兩個關(guān)鍵字來保證線程之間操作的有序性
- volatile關(guān)鍵字:本身就包含了禁止指令重排序的語義
- synchronized:是由“一個變量在同一時刻只允許一條線成對其進行l(wèi)ock操作”這條規(guī)則獲得的。這條規(guī)則決定了持有同一個鎖的兩個同步塊只能串行的進行。
synchronized關(guān)鍵字在這三種特性都可以作為一種解決方案。
2.6 先行發(fā)生原則happens-before
它是判斷數(shù)據(jù)是否存在競爭、線程是否安全的重要依據(jù)。
它是JMM中定義的兩項操作之間的偏序關(guān)系
- 如果說操作A先行發(fā)生于操作B,其實就是在說發(fā)生在操作B之前,操作A產(chǎn)生的影響能被操作B觀察到。
- 影響:包括修改了內(nèi)存中的值、發(fā)送了消息、調(diào)用了方法等。
- 舉例:
分析:
假設線程A的操作“i=1”先行發(fā)生于線程B的操作“j=i”,那么可以確定在線程B的操作執(zhí)行之后,變量j的值一定等于1。得出這個結(jié)論的依據(jù)有兩個:
- 根據(jù)先行發(fā)生原則,“i=1”的結(jié)果可以被觀察到;
- 線程C還沒登場,A操作結(jié)束后,沒有其他線程會修改變量i的值。
再考慮線程C,依然保持A和B之間的先行發(fā)生關(guān)系,而C出現(xiàn)在A和B之間,但是C和B沒有先行發(fā)生關(guān)系,那j的值會使多少?
答案是不確定。1和2都有可能。
- C對i的影響可能會被B觀察到,也可能不會
- 這時候,B就存在讀取到過期數(shù)據(jù)的風險,不具備多線程安全性。
JMM的天然的先行發(fā)生關(guān)系
無需任何同步器的協(xié)助就已經(jīng)存在,可在編碼中直接利用。
如果兩個操作之間的關(guān)系不在此列,并且無法從下列推導出來的話,它們就沒有順序性保障,虛擬機可以隨意地對它們進行重排序。
- 程序次序規(guī)則(program order rule):在一個線程內(nèi),按照程序代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作。準確的說,應該是控制流順序而不是程序代碼的順序,因為要考慮分支、循環(huán)等結(jié)構(gòu)。
- 管程鎖定規(guī)則(monitor lock rule):一個unlock操作先行發(fā)生于后面對同一個鎖的lock操作。這里必須強調(diào)的是同一個鎖,而“后面”是指時間上的先后順序。
- volatile變量規(guī)則(volatile variables rule):對一個volatile變量的寫操作先行發(fā)生于后面對這個變量的讀操作。這里的“后面”是指時間上的額先后順序。
- 線程啟動規(guī)則(thread start rule):Thread獨享的start()方法先行發(fā)生于此線程的每一個動作。
- 線程終止規(guī)則(Thread termination rule):線程中的所有操作都先行發(fā)生于對此線程的終止檢測。可通過Thread.join()方法結(jié)束、Thread.isAlive()的返回值等手段檢測到線程已經(jīng)終止。
- 線程中斷檢測(thread interruption rule):對縣城interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷時間的發(fā)生,可通過Thread.interrupted()方法檢測到是否有中斷發(fā)生。
- 對象終結(jié)規(guī)則(finalizer rule):一個對象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)先行發(fā)生于它的finalizer()方法的開始。
- 傳遞性(transitivity):如果操作A先行發(fā)生于操作BB,操作B先行發(fā)生于操作C,那就可以得出操作A先行發(fā)生于操作C的結(jié)論。
“時間上的先后順序”和“先行發(fā)生于”有何不同?
private int value=0;public void setValue(int value){this.value=value; }public int getValue(){return value; }假設存在線程A和B,A先(時間上的先后)調(diào)用了setValue(1),然后B調(diào)用了同一個對象的getValue(),那么B的返回值是什么?
一次分析下先行發(fā)生原則中的各項規(guī)則:
- A和B不在一個線程中,程序次序原則不適用;
- 沒有同步塊,不會發(fā)生lock和unclock操作,管程鎖定規(guī)則不適用;
- value變量沒有被volatile修飾,volatile變量規(guī)則不適用,
- 線程啟動/終止/終端規(guī)則和對象終結(jié)規(guī)則不適用
- 無適用的先行發(fā)生規(guī)則。
所以可以斷定,即使A在操作時間上先行發(fā)生于B,但是無法確定B中的gertValue()方法的返回結(jié)果。即這里面的操作不是線程安全的。
修復這個問題的方法:
- 把getValue()和setValue()都設置為synchronized方法,可套用管程鎖定規(guī)則。
- 把value定義為volatile,由于setValue()對value的修改不依賴原值,滿足volatile關(guān)鍵字使用場景,可套用volatile白能量規(guī)則來實現(xiàn)先行發(fā)生關(guān)系。
由上面的例子可以得出一個結(jié)論:
- 一個操作“時間上的先發(fā)生”不代表這個操作會是“先行發(fā)生”。
- 一個操作若是“先行發(fā)生”也不代表這個操作是“時間上先發(fā)生”。典型的例子就是“指令重排”
依據(jù)程序次序規(guī)則,“int i=1”的操作先行發(fā)生于“int j=2”。但是“int j=2”的代碼完全可能被處理器先執(zhí)行,這并不影響先行發(fā)生原則的正確性。因為我們無法再這條線程中感知到這一點。
上面兩個例子,綜合起來可得到:
時間先后順序和先行發(fā)生原則之間基本沒有太大的關(guān)系。
所以我們衡量并發(fā)安全問題的時候不要受到時間順序的干擾,一切必須以現(xiàn)行發(fā)生原則為準。
3. JVM參數(shù)中的Server模式和Client模式
具體見《java并發(fā)編程實戰(zhàn)》P31
對于服務器應用程序,無論是開發(fā)階段還是測試階段,當啟動JVM時一定都要指定-server命令行選項。
Server模式的JVM比client模式的JVM進行更多的優(yōu)化,例如將循環(huán)中未被修改的變量提升到循環(huán)外部,因此在開發(fā)模式(client模式的JVM)中能正確運行的代碼,可能會在部署環(huán)境(server模式的JVM)中運行失敗。如下代碼:
?
如果在代碼中忘記把asleep變量聲明為volatile變量,則Server模式的JVM會把asleep變量的判斷提升到循環(huán)體外部(這將導致一個無限循環(huán)),但Client模式的JVM不會這么做。
在解決開發(fā)環(huán)境中出現(xiàn)無限循環(huán)問題時,解決這個問題的開銷遠小于解決在應用環(huán)境中出現(xiàn)無限循環(huán)的開銷。
整理自《深入理解Java虛擬機——JVM高級特性與最佳實踐》
總結(jié)
以上是生活随笔為你收集整理的Java内存模型、volatile、原子性、可见性、有序性、happens-before原则的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java内存泄露和内存溢出、JVM命令行
- 下一篇: java美元兑换,(Java实现) 美元