volatile兩大作用
一旦一個共享變量(類的成員變量荒揣、類的靜態(tài)成員變量)被volatile修飾之后,那么就具備了兩層語義:1)保證了不同線程對這個變量進行操作時的可見性蛙埂,即一個線程修改了某個變量的值图贸,這新值對其他線程來說是立即可見的。
2)禁止進行指令重排序乾闰。
需注意volatile并不保證操作的原子性落追。
(一)內存可見性
1 概念
JVM內存模型(JMM):主內存和線程獨立的工作內存
Java內存模型規(guī)定,對于多個線程共享的變量涯肩,存儲在主內存當中轿钠,每個線程都有自己獨立的工作內存(比如CPU的寄存器、CPU緩存)病苗,線程只能訪問自己的工作內存疗垛,不可以訪問其它線程的工作內存。
工作內存中保存了主內存共享變量的副本硫朦,線程要操作這些共享變量贷腕,只能通過操作工作內存中的副本來實現(xiàn),操作完畢之后再同步回到主內存當中。
如何保證多個線程操作主內存的數(shù)據(jù)完整性是一個難題泽裳,Java內存模型也規(guī)定了工作內存與主內存之間交互的協(xié)議瞒斩,定義了8種原子操作:
(1) lock:將主內存中的變量鎖定,為一個線程所獨占
(2) unclock:將lock加的鎖定解除涮总,此時其它的線程可以有機會訪問此變量
(3) read:將主內存中的變量值讀到工作內存當中
(4) load:將read讀取的值保存到工作內存中的變量副本中胸囱。
(5) use:將值傳遞給線程的代碼執(zhí)行引擎
(6) assign:將執(zhí)行引擎處理返回的值重新賦值給變量副本
(7) store:將變量副本的值存儲到主內存中。
(8) write:將store存儲的值寫入到主內存的共享變量當中妹卿。
通過上面Java內存模型的概述旺矾,我們會注意到這么一個問題,每個線程在獲取鎖之后會在自己的工作內存來操作共享變量夺克,操作完成之后將工作內存中的副本回寫到主內存箕宙,并且在其它線程從主內存將變量同步回自己的工作內存之前,共享變量的改變對其是不可見的铺纽。即其他線程的本地內存中的變量已經是過時的柬帕,并不是更新后的值。
(二)指令重排
1 概念
?在執(zhí)行程序時狡门,為了提高性能陷寝,編譯器和處理器常常會對指令做重排序。重排序分3種類型:編譯器優(yōu)化的重排序其馏。編譯器在不改變單線程程序語義的前提下凤跑,可以重新安排語句的執(zhí)行順序。指令級并行的重排序∨迅矗現(xiàn)代處理器采用了指令級并行技術(Instruction-LevelParallelism仔引,ILP)來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性褐奥,處理器可以改變語句對應機器指令的執(zhí)行順序咖耘。內存系統(tǒng)的重排序。由于處理器使用緩存和讀/寫緩沖區(qū)撬码,這使得加載和存儲操作看上去可能是在亂序執(zhí)行儿倒。
?從Java源代碼到最終實際執(zhí)行的指令序列,會分別經歷下面3種重排序:?
對于編譯器呜笑,JMM的編譯器重排序規(guī)則會禁止特定類型的編譯器重排序(不是所有的編譯器重排序都要禁止)夫否。對于處理器重排序,JMM的處理器重排序規(guī)則會要求Java編譯器在生成指令序列時叫胁,插入特定類型的內存屏障(Memory Barriers慷吊,Intel稱之為Memory Fence)指令,通過內存屏障指令來禁止特定類型的處理器重排序曹抬。JMM屬于語言級的內存模型,它確保在不同的編譯器和不同的處理器平臺之上,通過禁止特定類型的編譯器重排序和處理器重排序谤民,為程序員提供一致的內存可見性保證堰酿。
指令重排序是JVM為了優(yōu)化指令,提高程序運行效率张足,在不影響單線程程序執(zhí)行結果的前提下触创,盡可能地提高并行度。編譯器为牍、處理器也遵循這樣一個目標哼绑。注意是單線程。多線程的情況下指令重排序就會給程序員帶來問題碉咆。
不同的指令間可能存在數(shù)據(jù)依賴抖韩。比如下面計算圓的面積的語句:
double r = 2.3d;//(1)
double pi =3.1415926; //(2)
double area = pi* r * r; //(3)
area的計算依賴于r與pi兩個變量的賦值指令。而r與pi無依賴關系疫铜。
as-if-serial語義是指:不管如何重排序(編譯器與處理器為了提高并行度)茂浮,(單線程)程序的結果不能被改變。這是編譯器壳咕、Runtime席揽、處理器必須遵守的語義。
雖然谓厘,(1) – happensbefore -> (2),(2) – happens before -> (3)幌羞,但是計算順序(1)(2)(3)與(2)(1)(3) 對于r、pi竟稳、area變量的結果并無區(qū)別属桦。編譯器、Runtime在優(yōu)化時可以根據(jù)情況重排序(1)與(2)住练,而絲毫不影響程序的結果地啰。
指令重排序包括編譯器重排序和運行時重排序。
2 指令重排帶來的問題
如果一個操作不是原子的讲逛,就會給JVM留下重排的機會亏吝。下面看幾個例子:
例子1:A線程指令重排導致B線程出錯
對于在同一個線程內,這樣的改變是不會對邏輯產生影響的盏混,但是在多線程的情況下指令重排序會帶來問題蔚鸥。看下面這個情景:
在線程A中:
context = loadContext();
inited = true;
在線程B中:
while(!inited ){ //根據(jù)線程A中對inited變量的修改決定是否使用context變量
???sleep(100);
}
doSomethingwithconfig(context);
假設線程A中發(fā)生了指令重排序:
inited = true;
context = loadContext();
那么B中很可能就會拿到一個尚未初始化或尚未初始化完成的context,從而引發(fā)程序錯誤许赃。
例子2:指令重排導致單例模式失效
我們都知道一個經典的懶加載方式的雙重判斷單例模式:
public class Singleton {
??private static Singleton instance = null;
??private Singleton() { }
??public static Singleton getInstance() {
?????if(instance == null) {
????????synchronzied(Singleton.class) {
???????????if(instance == null) {
? ? ? ? ? ? ? ?instance = new Singleton();? //非原子操作
???????????}
????????}
?????}
?????return instance;
???}
}
看似簡單的一段賦值語句:instance= new Singleton()止喷,但是很不幸它并不是一個原子操作,其實際上可以抽象為下面幾條JVM指令:
memory =allocate();??? //1:分配對象的內存空間?
ctorInstance(memory);? //2:初始化對象?
instance =memory;???? //3:設置instance指向剛分配的內存地址
上面操作2依賴于操作1混聊,但是操作3并不依賴于操作2弹谁,所以JVM是可以針對它們進行指令的優(yōu)化重排序的,經過重排序后如下:
memory =allocate();??? //1:分配對象的內存空間?
instance =memory;???? //3:instance指向剛分配的內存地址,此時對象還未初始化
ctorInstance(memory);? //2:初始化對象
可以看到指令重排之后预愤,instance指向分配好的內存放在了前面沟于,而這段內存的初始化被排在了后面。
在線程A執(zhí)行這段賦值語句植康,在初始化分配對象之前就已經將其賦值給instance引用旷太,恰好另一個線程進入方法判斷instance引用不為null,然后就將其返回使用销睁,導致出錯供璧。
3 防止指令重排
除了前面內存可見性中講到的volatile關鍵字可以保證變量修改的可見性之外,還有另一個重要的作用:在JDK1.5之后冻记,可以使用volatile變量禁止指令重排序睡毒。
解決方案:例子1中的inited和例子2中的instance以關鍵字volatile修飾之后,就會阻止JVM對其相關代碼進行指令重排檩赢,這樣就能夠按照既定的順序指執(zhí)行吕嘀。
volatile關鍵字通過提供“內存屏障”的方式來防止指令被重排序,為了實現(xiàn)volatile的內存語義贞瞒,編譯器在生成字節(jié)碼時偶房,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。
大多數(shù)的處理器都支持內存屏障的指令军浆。
對于編譯器來說棕洋,發(fā)現(xiàn)一個最優(yōu)布置來最小化插入屏障的總數(shù)幾乎不可能,為此乒融,Java內存模型采取保守策略掰盘。下面是基于保守策略的JMM內存屏障插入策略:
在每個volatile寫操作的前面插入一個StoreStore屏障。
在每個volatile寫操作的后面插入一個StoreLoad屏障赞季。
在每個volatile讀操作的后面插入一個LoadLoad屏障愧捕。
在每個volatile讀操作的后面插入一個LoadStore屏障。
(三)總結
volatile是輕量級同步機制
相對于synchronized塊的代碼鎖申钩,volatile應該是提供了一個輕量級的針對共享變量的鎖次绘,當我們在多個線程間使用共享變量進行通信的時候需要考慮將共享變量用volatile來修飾。
volatile是一種稍弱的同步機制撒遣,在訪問volatile變量時不會執(zhí)行加鎖操作邮偎,也就不會執(zhí)行線程阻塞,因此volatilei變量是一種比synchronized關鍵字更輕量級的同步機制义黎。
volatile使用建議
使用建議:在兩個或者更多的線程需要訪問的成員變量上使用volatile禾进。當要訪問的變量已在synchronized代碼塊中,或者為常量時廉涕,沒必要使用volatile泻云。
由于使用volatile屏蔽掉了JVM中必要的代碼優(yōu)化艇拍,所以在效率上比較低,因此一定在必要時才使用此關鍵字宠纯。
volatile和synchronized區(qū)別
1淑倾、volatile不會進行加鎖操作:
volatile變量是一種稍弱的同步機制在訪問volatile變量時不會執(zhí)行加鎖操作,因此也就不會使執(zhí)行線程阻塞征椒,因此volatile變量是一種比synchronized關鍵字更輕量級的同步機制。
2湃累、volatile變量作用類似于同步變量讀寫操作:
從內存可見性的角度看勃救,寫入volatile變量相當于退出同步代碼塊,而讀取volatile變量相當于進入同步代碼塊治力。
3蒙秒、volatile不如synchronized安全:
在代碼中如果過度依賴volatile變量來控制狀態(tài)的可見性,通常會比使用鎖的代碼更脆弱宵统,也更難以理解晕讲。僅當volatile變量能簡化代碼的實現(xiàn)以及對同步策略的驗證時,才應該使用它马澈。一般來說瓢省,用同步機制會更安全些。
4痊班、volatile無法同時保證內存可見性和原則性:
加鎖機制(即同步機制)既可以確鼻诨椋可見性又可以確保原子性,而volatile變量只能確钡臃ィ可見性馒胆,原因是聲明為volatile的簡單變量如果當 前值與該變量以前的值相關,那么volatile關鍵字不起作用凝果,也就是說如下的表達式都不是原子操作:“count++”祝迂、“count = count+1”。
當且僅當滿足以下所有條件時器净,才應該使用volatile變量:
1型雳、 對變量的寫入操作不依賴變量的當前值,或者你能確保只有單個線程更新變量的值掌动。
2四啰、該變量沒有包含在具有其他變量的不變式中。
在需要同步的時候粗恢,第一選擇應該是synchronized關鍵字柑晒,這是最安全的方式,嘗試其他任何方式都是有風險的眷射。尤其在匙赞、jdK1.5 之后佛掖,對synchronized同步機制做了很多優(yōu)化,如:自適應的自旋鎖涌庭、鎖粗化芥被、鎖消除、輕量級鎖等坐榆,使得它的性能明顯有了很大的提升拴魄。volatile相對于synchronized稍微輕量 些,在某些場合它可以替代synchronized席镀,但是又不能完全取代synchronized匹中,只有在某些場合才能夠使用volatile。volatile經常用于兩個場景:狀態(tài)標記豪诲、double check顶捷。