Java并发:volatile内存可见性和指令重排
volatile兩大作用
1、保證內(nèi)存可見(jiàn)性
2、防止指令重排
此外需注意volatile并不保證操作的原子性。
(一)內(nèi)存可見(jiàn)性
1 概念
JVM內(nèi)存模型:主內(nèi)存和線(xiàn)程獨(dú)立的工作內(nèi)存
Java內(nèi)存模型規(guī)定,對(duì)于多個(gè)線(xiàn)程共享的變量,存儲(chǔ)在主內(nèi)存當(dāng)中,每個(gè)線(xiàn)程都有自己獨(dú)立的工作內(nèi)存(比如CPU的寄存器),線(xiàn)程只能訪(fǎng)問(wèn)自己的工作內(nèi)存,不可以訪(fǎng)問(wèn)其它線(xiàn)程的工作內(nèi)存。
工作內(nèi)存中保存了主內(nèi)存共享變量的副本,線(xiàn)程要操作這些共享變量,只能通過(guò)操作工作內(nèi)存中的副本來(lái)實(shí)現(xiàn),操作完畢之后再同步回到主內(nèi)存當(dāng)中。
如何保證多個(gè)線(xiàn)程操作主內(nèi)存的數(shù)據(jù)完整性是一個(gè)難題,Java內(nèi)存模型也規(guī)定了工作內(nèi)存與主內(nèi)存之間交互的協(xié)議,定義了8種原子操作:
(1) lock:將主內(nèi)存中的變量鎖定,為一個(gè)線(xiàn)程所獨(dú)占
(2) unclock:將lock加的鎖定解除,此時(shí)其它的線(xiàn)程可以有機(jī)會(huì)訪(fǎng)問(wèn)此變量
(3) read:將主內(nèi)存中的變量值讀到工作內(nèi)存當(dāng)中
(4) load:將read讀取的值保存到工作內(nèi)存中的變量副本中。
(5) use:將值傳遞給線(xiàn)程的代碼執(zhí)行引擎
(6) assign:將執(zhí)行引擎處理返回的值重新賦值給變量副本
(7) store:將變量副本的值存儲(chǔ)到主內(nèi)存中。
(8) write:將store存儲(chǔ)的值寫(xiě)入到主內(nèi)存的共享變量當(dāng)中。
通過(guò)上面Java內(nèi)存模型的概述,我們會(huì)注意到這么一個(gè)問(wèn)題,每個(gè)線(xiàn)程在獲取鎖之后會(huì)在自己的工作內(nèi)存來(lái)操作共享變量,操作完成之后將工作內(nèi)存中的副本回寫(xiě)到主內(nèi)存,并且在其它線(xiàn)程從主內(nèi)存將變量同步回自己的工作內(nèi)存之前,共享變量的改變對(duì)其是不可見(jiàn)的。即其他線(xiàn)程的本地內(nèi)存中的變量已經(jīng)是過(guò)時(shí)的,并不是更新后的值。
2 內(nèi)存可見(jiàn)性帶來(lái)的問(wèn)題
很多時(shí)候我們需要一個(gè)線(xiàn)程對(duì)共享變量的改動(dòng),其它線(xiàn)程也需要立即得知這個(gè)改動(dòng)該怎么辦呢?下面舉兩個(gè)例子說(shuō)明內(nèi)存可見(jiàn)性的重要性:
例子1
有一個(gè)全局的狀態(tài)變量open:
| 1 | booleanopen=true; |
這個(gè)變量用來(lái)描述對(duì)一個(gè)資源的打開(kāi)關(guān)閉狀態(tài),true表示打開(kāi),false表示關(guān)閉,假設(shè)有一個(gè)線(xiàn)程A,在執(zhí)行一些操作后將open修改為false:
| 1 2 3 | <strong>//線(xiàn)程A resource.close(); open = false; |
線(xiàn)程B隨時(shí)關(guān)注open的狀態(tài),當(dāng)open為true的時(shí)候通過(guò)訪(fǎng)問(wèn)資源來(lái)進(jìn)行一些操作:
| 1 2 3 4 | <strong>//線(xiàn)程B while(open) { doSomethingWithResource(resource); } |
當(dāng)A把資源關(guān)閉的時(shí)候,open變量對(duì)線(xiàn)程B是不可見(jiàn)的,如果此時(shí)open變量的改動(dòng)尚未同步到線(xiàn)程B的工作內(nèi)存中,那么線(xiàn)程B就會(huì)用一個(gè)已經(jīng)關(guān)閉了的資源去做一些操作,因此產(chǎn)生錯(cuò)誤。
例子2
下面是一個(gè)通過(guò)布爾標(biāo)志判斷線(xiàn)程是否結(jié)束的例子:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | publicclass CancelThreadTest { ?????????publicstaticvoidmain(String[] args) throwsException{ ???????????????????PrimeGeneratorgen = newPrimeGenerator(); ???????????????????newThread(gen).start(); ???????????????????try ???????????????????{ ????????????????????????????Thread.sleep(3000); ???????????????????}finally{ ????????????????????????????gen.cancel(); ???????????????????} ?????????} } classPrimeGenerator implementsRunnable{ ?????????privateboolean cancelled;?????? ?????????@Override ?????????publicvoid run() { ???????????????????while(!cancelled) ???????????????????{ ????????????????????????????System.out.println("Running..."); ????????????????????????????//doingsomething here... ???????????????????}??????? ???????? ?????????}??????? ?????????publicvoid cancel(){cancelled = true;} } |
主線(xiàn)程中設(shè)置PrimeGenerator線(xiàn)程的是否取消標(biāo)識(shí),PrimeGenerator線(xiàn)程檢測(cè)到這個(gè)標(biāo)識(shí)后就會(huì)結(jié)束線(xiàn)程,由于主線(xiàn)程修改cancelled變量的內(nèi)存可見(jiàn)性,主線(xiàn)程修改cancelled標(biāo)識(shí)后并不馬上同步回主內(nèi)存,所以PrimeGenerator線(xiàn)程結(jié)束的時(shí)間難以把控(最終是一定會(huì)同步回主內(nèi)存,讓PrimeGenerator線(xiàn)程結(jié)束)。
如果PrimeGenerator線(xiàn)程執(zhí)行一些比較關(guān)鍵的操作,主線(xiàn)程希望能夠及時(shí)終止它,這時(shí)將cenceled用volatile關(guān)鍵字修飾就是必要的。
特別注意:上面演示這個(gè)并不是正確的取消線(xiàn)程的方法,因?yàn)橐坏㏄rimeGenerator線(xiàn)程中包含BolckingQueue.put()等阻塞方法,那么將可能永遠(yuǎn)不會(huì)去檢查cancelled標(biāo)識(shí),導(dǎo)致線(xiàn)程永遠(yuǎn)不會(huì)退出。正確的方法參見(jiàn)另外一篇關(guān)于如何正確終止線(xiàn)程的方法。
3 提供內(nèi)存可見(jiàn)性
volatile保證可見(jiàn)性的原理是在每次訪(fǎng)問(wèn)變量時(shí)都會(huì)進(jìn)行一次刷新,因此每次訪(fǎng)問(wèn)都是主內(nèi)存中最新的版本。所以volatile關(guān)鍵字的作用之一就是保證變量修改的實(shí)時(shí)可見(jiàn)性。
針對(duì)上面的例子1:
要求一個(gè)線(xiàn)程對(duì)open的改變,其他的線(xiàn)程能夠立即可見(jiàn),Java為此提供了volatile關(guān)鍵字,在聲明open變量的時(shí)候加入volatile關(guān)鍵字就可以保證open的內(nèi)存可見(jiàn)性,即open的改變對(duì)所有的線(xiàn)程都是立即可見(jiàn)的。
針對(duì)上面的例子2:
將cancelled標(biāo)志設(shè)置的volatile保證主線(xiàn)程針對(duì)cancelled標(biāo)識(shí)的修改能夠讓PrimeGenerator線(xiàn)程立馬看到。
備注:也可以通過(guò)提供synchronized同步的open變量的Get/Set方法解決此內(nèi)存可見(jiàn)性問(wèn)題,因?yàn)橐狦et變量open,必須等Set方完全釋放鎖之后。后面將介紹到兩者的區(qū)別。
(二)指令重排
1 概念
指令重排序是JVM為了優(yōu)化指令,提高程序運(yùn)行效率,在不影響單線(xiàn)程程序執(zhí)行結(jié)果的前提下,盡可能地提高并行度。編譯器、處理器也遵循這樣一個(gè)目標(biāo)。注意是單線(xiàn)程。多線(xiàn)程的情況下指令重排序就會(huì)給程序員帶來(lái)問(wèn)題。
不同的指令間可能存在數(shù)據(jù)依賴(lài)。比如下面計(jì)算圓的面積的語(yǔ)句:
| 1 2 3 | doubler = 2.3d;//(1) doublepi =3.1415926;//(2) doublearea = pi* r * r; //(3) |
area的計(jì)算依賴(lài)于r與pi兩個(gè)變量的賦值指令。而r與pi無(wú)依賴(lài)關(guān)系。
as-if-serial語(yǔ)義是指:不管如何重排序(編譯器與處理器為了提高并行度),(單線(xiàn)程)程序的結(jié)果不能被改變。這是編譯器、Runtime、處理器必須遵守的語(yǔ)義。
雖然,(1) – happensbefore -> (2),(2) – happens before -> (3),但是計(jì)算順序(1)(2)(3)與(2)(1)(3) 對(duì)于r、pi、area變量的結(jié)果并無(wú)區(qū)別。編譯器、Runtime在優(yōu)化時(shí)可以根據(jù)情況重排序(1)與(2),而絲毫不影響程序的結(jié)果。
指令重排序包括編譯器重排序和運(yùn)行時(shí)重排序。
2 指令重排帶來(lái)的問(wèn)題
如果一個(gè)操作不是原子的,就會(huì)給JVM留下重排的機(jī)會(huì)。下面看幾個(gè)例子:
例子1:A線(xiàn)程指令重排導(dǎo)致B線(xiàn)程出錯(cuò)
對(duì)于在同一個(gè)線(xiàn)程內(nèi),這樣的改變是不會(huì)對(duì)邏輯產(chǎn)生影響的,但是在多線(xiàn)程的情況下指令重排序會(huì)帶來(lái)問(wèn)題。看下面這個(gè)情景:
在線(xiàn)程A中:
| 1 2 | context = loadContext(); inited = true; |
在線(xiàn)程B中:
| 1 2 3 4 | while(!inited ){ //根據(jù)線(xiàn)程A中對(duì)inited變量的修改決定是否使用context變量 ???sleep(100); } doSomethingwithconfig(context); |
假設(shè)線(xiàn)程A中發(fā)生了指令重排序:
| 1 2 | inited = true; context = loadContext(); |
那么B中很可能就會(huì)拿到一個(gè)尚未初始化或尚未初始化完成的context,從而引發(fā)程序錯(cuò)誤。
例子2:指令重排導(dǎo)致單例模式失效
我們都知道一個(gè)經(jīng)典的懶加載方式的雙重判斷單例模式:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | publicclass Singleton { ??privatestatic Singleton instance = null; ??privateSingleton() { } ??publicstatic Singleton getInstance() { ?????if(instance == null) { ????????synchronzied(Singleton.class) { ???????????if(instance == null) { ???????????????<strong>instance = newSingleton();? //非原子操作 ???????????} ????????} ?????} ?????returninstance; ???} } |
看似簡(jiǎn)單的一段賦值語(yǔ)句:instance= new Singleton(),但是很不幸它并不是一個(gè)原子操作,其實(shí)際上可以抽象為下面幾條JVM指令:
| 1 2 3 | memory =allocate();??? //1:分配對(duì)象的內(nèi)存空間? ctorInstance(memory);?//2:初始化對(duì)象? instance =memory;???? //3:設(shè)置instance指向剛分配的內(nèi)存地址 |
上面操作2依賴(lài)于操作1,但是操作3并不依賴(lài)于操作2,所以JVM是可以針對(duì)它們進(jìn)行指令的優(yōu)化重排序的,經(jīng)過(guò)重排序后如下:
| 1 2 3 | memory =allocate();??? //1:分配對(duì)象的內(nèi)存空間? instance =memory;???? //3:instance指向剛分配的內(nèi)存地址,此時(shí)對(duì)象還未初始化 ctorInstance(memory);?//2:初始化對(duì)象 |
可以看到指令重排之后,instance指向分配好的內(nèi)存放在了前面,而這段內(nèi)存的初始化被排在了后面。
在線(xiàn)程A執(zhí)行這段賦值語(yǔ)句,在初始化分配對(duì)象之前就已經(jīng)將其賦值給instance引用,恰好另一個(gè)線(xiàn)程進(jìn)入方法判斷instance引用不為null,然后就將其返回使用,導(dǎo)致出錯(cuò)。
3 防止指令重排
除了前面內(nèi)存可見(jiàn)性中講到的volatile關(guān)鍵字可以保證變量修改的可見(jiàn)性之外,還有另一個(gè)重要的作用:在JDK1.5之后,可以使用volatile變量禁止指令重排序。??
解決方案:例子1中的inited和例子2中的instance以關(guān)鍵字volatile修飾之后,就會(huì)阻止JVM對(duì)其相關(guān)代碼進(jìn)行指令重排,這樣就能夠按照既定的順序指執(zhí)行。
volatile關(guān)鍵字通過(guò)提供“內(nèi)存屏障”的方式來(lái)防止指令被重排序,為了實(shí)現(xiàn)volatile的內(nèi)存語(yǔ)義,編譯器在生成字節(jié)碼時(shí),會(huì)在指令序列中插入內(nèi)存屏障來(lái)禁止特定類(lèi)型的處理器重排序。
大多數(shù)的處理器都支持內(nèi)存屏障的指令。
對(duì)于編譯器來(lái)說(shuō),發(fā)現(xiàn)一個(gè)最優(yōu)布置來(lái)最小化插入屏障的總數(shù)幾乎不可能,為此,Java內(nèi)存模型采取保守策略。下面是基于保守策略的JMM內(nèi)存屏障插入策略:
在每個(gè)volatile寫(xiě)操作的前面插入一個(gè)StoreStore屏障。
在每個(gè)volatile寫(xiě)操作的后面插入一個(gè)StoreLoad屏障。
在每個(gè)volatile讀操作的后面插入一個(gè)LoadLoad屏障。
在每個(gè)volatile讀操作的后面插入一個(gè)LoadStore屏障。
(三)總結(jié)
volatile是輕量級(jí)同步機(jī)制
相對(duì)于synchronized塊的代碼鎖,volatile應(yīng)該是提供了一個(gè)輕量級(jí)的針對(duì)共享變量的鎖,當(dāng)我們?cè)诙鄠€(gè)線(xiàn)程間使用共享變量進(jìn)行通信的時(shí)候需要考慮將共享變量用volatile來(lái)修飾。
volatile是一種稍弱的同步機(jī)制,在訪(fǎng)問(wèn)volatile變量時(shí)不會(huì)執(zhí)行加鎖操作,也就不會(huì)執(zhí)行線(xiàn)程阻塞,因此volatilei變量是一種比synchronized關(guān)鍵字更輕量級(jí)的同步機(jī)制。
volatile使用建議
使用建議:在兩個(gè)或者更多的線(xiàn)程需要訪(fǎng)問(wèn)的成員變量上使用volatile。當(dāng)要訪(fǎng)問(wèn)的變量已在synchronized代碼塊中,或者為常量時(shí),沒(méi)必要使用volatile。
由于使用volatile屏蔽掉了JVM中必要的代碼優(yōu)化,所以在效率上比較低,因此一定在必要時(shí)才使用此關(guān)鍵字。
volatile和synchronized區(qū)別
1、volatile不會(huì)進(jìn)行加鎖操作:
volatile變量是一種稍弱的同步機(jī)制在訪(fǎng)問(wèn)volatile變量時(shí)不會(huì)執(zhí)行加鎖操作,因此也就不會(huì)使執(zhí)行線(xiàn)程阻塞,因此volatile變量是一種比synchronized關(guān)鍵字更輕量級(jí)的同步機(jī)制。
2、volatile變量作用類(lèi)似于同步變量讀寫(xiě)操作:
從內(nèi)存可見(jiàn)性的角度看,寫(xiě)入volatile變量相當(dāng)于退出同步代碼塊,而讀取volatile變量相當(dāng)于進(jìn)入同步代碼塊。
3、volatile不如synchronized安全:
在代碼中如果過(guò)度依賴(lài)volatile變量來(lái)控制狀態(tài)的可見(jiàn)性,通常會(huì)比使用鎖的代碼更脆弱,也更難以理解。僅當(dāng)volatile變量能簡(jiǎn)化代碼的實(shí)現(xiàn)以及對(duì)同步策略的驗(yàn)證時(shí),才應(yīng)該使用它。一般來(lái)說(shuō),用同步機(jī)制會(huì)更安全些。
4、volatile無(wú)法同時(shí)保證內(nèi)存可見(jiàn)性和原則性:
加鎖機(jī)制(即同步機(jī)制)既可以確保可見(jiàn)性又可以確保原子性,而volatile變量只能確保可見(jiàn)性,原因是聲明為volatile的簡(jiǎn)單變量如果當(dāng)前值與該變量以前的值相關(guān),那么volatile關(guān)鍵字不起作用,也就是說(shuō)如下的表達(dá)式都不是原子操作:“count++”、“count = count+1”。
當(dāng)且僅當(dāng)滿(mǎn)足以下所有條件時(shí),才應(yīng)該使用volatile變量:
1、 對(duì)變量的寫(xiě)入操作不依賴(lài)變量的當(dāng)前值,或者你能確保只有單個(gè)線(xiàn)程更新變量的值。
2、該變量沒(méi)有包含在具有其他變量的不變式中。
總結(jié):在需要同步的時(shí)候,第一選擇應(yīng)該是synchronized關(guān)鍵字,這是最安全的方式,嘗試其他任何方式都是有風(fēng)險(xiǎn)的。尤其在、jdK1.5之后,對(duì)synchronized同步機(jī)制做了很多優(yōu)化,如:自適應(yīng)的自旋鎖、鎖粗化、鎖消除、輕量級(jí)鎖等,使得它的性能明顯有了很大的提升。
from:?http://www.importnew.com/23535.html
總結(jié)
以上是生活随笔為你收集整理的Java并发:volatile内存可见性和指令重排的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 多线程之线程池的应用
- 下一篇: 谈谈Java反射机制