日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪(fǎng)問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程语言 > java >内容正文

java

Java并发:volatile内存可见性和指令重排

發(fā)布時(shí)間:2025/3/21 java 36 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Java并发:volatile内存可见性和指令重排 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

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)題。

如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。