并發(fā)環(huán)境下指令重排帶來的問題

JVM內(nèi)存模型 - 主內(nèi)存和線程獨(dú)立的工作內(nèi)存
Java內(nèi)存模型規(guī)定简珠,對于多個線程共享的變量外盯,存儲在主內(nèi)存當(dāng)中,每個線程都有自己獨(dú)立的工作內(nèi)存挺尿,線程只能訪問自己的工作內(nèi)存奏黑,不可以訪問其它線程的工作內(nèi)存。工作內(nèi)存中保存了主內(nèi)存共享變量的副本编矾,線程要操作這些共享變量熟史,只能通過操作工作內(nèi)存中的副本來實現(xiàn),操作完畢之后再同步回到主內(nèi)存當(dāng)中窄俏。
如何保證多個線程操作主內(nèi)存的數(shù)據(jù)完整性是一個難題蹂匹,Java內(nèi)存模型也規(guī)定了工作內(nèi)存與主內(nèi)存之間交互的協(xié)議,首先是定義了8種原子操作:
(1) lock:將主內(nèi)存中的變量鎖定凹蜈,為一個線程所獨(dú)占
(2) unclock:將lock加的鎖定解除限寞,此時其它的線程可以有機(jī)會訪問此變量
(3) read:將主內(nèi)存中的變量值讀到工作內(nèi)存當(dāng)中
(4) load:將read讀取的值保存到工作內(nèi)存中的變量副本中。
(5) use:將值傳遞給線程的代碼執(zhí)行引擎
(6) assign:將執(zhí)行引擎處理返回的值重新賦值給變量副本
(7) store:將變量副本的值存儲到主內(nèi)存中踪区。
(8) write:將store存儲的值寫入到主內(nèi)存的共享變量當(dāng)中昆烁。

  1. 內(nèi)存可見性
    1.1 概念
    通過上面Java內(nèi)存模型的概述,我們會注意到這么一個問題缎岗,每個線程在獲取鎖之后會在自己的工作內(nèi)存來操作共享變量静尼,操作完成之后將工作內(nèi)存中的副本回寫到主內(nèi)存,并且在其它線程從主內(nèi)存將變量同步回自己的工作內(nèi)存之前,共享變量的改變對其是不可見的鼠渺。

1.2 內(nèi)存可見性帶來的問題
很多時候我們需要一個線程對共享變量的改動鸭巴,其它線程也需要立即得知這個改動該怎么辦呢?比如以下的情景拦盹,有一個全局的狀態(tài)變量open:
boolean open=true;
這個變量用來描述對一個資源的打開關(guān)閉狀態(tài)鹃祖,true表示打開,false表示關(guān)閉普舆,假設(shè)有一個線程A,在執(zhí)行一些操作后將open修改為false:
//線程A
resource.close();
open = false;
線程B隨時關(guān)注open的狀態(tài)恬口,當(dāng)open為true的時候通過訪問資源來進(jìn)行一些操作:
//線程B
while(open) {
doSomethingWithResource(resource);
}
當(dāng)A把資源關(guān)閉的時候,open變量對線程B不可見沼侣,如果此時open變量的改動尚未同步到線程B的工作內(nèi)存中,那么線程B就會用一個已經(jīng)關(guān)閉了的資源去做一些操作祖能,因此產(chǎn)生錯誤。
1.3 volatile關(guān)鍵字
所以對于上面的情景蛾洛,要求一個線程對open的改變养铸,其他的線程能夠立即可見,Java為此提供了volatile關(guān)鍵字轧膘,在聲明open變量的時候加入volatile關(guān)鍵字就可以保證open的內(nèi)存可見性钞螟,即open的改變對所有的線程都是立即可見的。
volatile保證可見性的原理是在每次訪問變量時都會進(jìn)行一次刷新谎碍,因此每次訪問都是主內(nèi)存中最新的版本鳞滨。所以volatile關(guān)鍵字的作用之一就是保證變量修改的實時可見性

  1. 指令重排
    2.1 概念
    指令重排序是JVM為了優(yōu)化指令椿浓,提高程序運(yùn)行效率太援。指令重排序包括編譯器重排序和運(yùn)行時重排序。JVM規(guī)范規(guī)定扳碍,指令重排序可以在不影響單線程程序執(zhí)行結(jié)果前提下進(jìn)行提岔。
    2.2 指令重排帶來的問題
    例子1:簡單指令重排
    假設(shè)有這么兩個共享變量a和b:
    private int a;
    private int b;
    在線程A中有兩條語句對這兩個共享變量進(jìn)行賦值操作:
    a = 1;
    b = 2;
    假設(shè)當(dāng)線程A對a進(jìn)行復(fù)制操作的時候發(fā)現(xiàn)這個變量在主內(nèi)存已經(jīng)被其它的線程加了訪問鎖,那么此時線程A怎么辦笋敞?等待釋放鎖碱蒙?不,等待太浪費(fèi)時間了夯巷,它會去嘗試進(jìn)行b的賦值操作赛惩,b這時候沒被人占用,因此就會先為b賦值趁餐,再去為a賦值喷兼,那么執(zhí)行的順序就變成了:
    b = 2;
    a = 1;
    例子2:A線程指令重排導(dǎo)致B線程出錯
    對于在同一個線程內(nèi),這樣的改變是不會對邏輯產(chǎn)生影響的后雷,但是在多線程的情況下指令重排序會帶來問題季惯》透鳎看下面這個情景:
    在線程A中:
    context = loadContext();
    inited = true;

在線程B中:
while(!inited ){ //根據(jù)線程A中對inited變量的修改決定是否使用context變量
sleep(100);
}
doSomethingwithconfig(context);
假設(shè)線程A中發(fā)生了指令重排序:
inited = true;
context = loadContext();
那么B中很可能就會拿到一個尚未初始化或尚未初始化完成的context,從而引發(fā)程序錯誤。
例子3:指令重排導(dǎo)致單例模式失效
我們都知道一個經(jīng)典的懶加載方式的單例模式:
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內(nèi)部已經(jīng)轉(zhuǎn)換為多條指令:
memory = allocate(); //1:分配對象的內(nèi)存空間
ctorInstance(memory); //2:初始化對象
instance = memory; //3:設(shè)置instance指向剛分配的內(nèi)存地址
但是經(jīng)過重排序后如下:
memory = allocate(); //1:分配對象的內(nèi)存空間
instance = memory; //3:設(shè)置instance指向剛分配的內(nèi)存地址贾漏,此時對象還沒被初始化
ctorInstance(memory); //2:初始化對象
可以看到指令重排之后,instance指向分配好的內(nèi)存放在了前面藕筋,而這段內(nèi)存的初始化被排在了后面纵散,在線程A初始化完成這段內(nèi)存之前,線程B雖然進(jìn)不去同步代碼塊隐圾,但是在同步代碼塊之前的判斷就會發(fā)現(xiàn)instance不為空伍掀,此時線程B獲得instance對象進(jìn)行使用就可能發(fā)生錯誤。
2.3 volatile關(guān)鍵字
除了前面內(nèi)存可見性中講到的volatile關(guān)鍵字可以保證變量修改的可見性之外暇藏,還有另一個重要的作用:在JDK1.5之后硕盹,可以使用volatile變量禁止指令重排序。
例子2和例子3中的變量以關(guān)鍵字volatile修飾之后叨咖,就會組織JVM對其相關(guān)代碼進(jìn)行指令重排,這樣就能夠按照既定的順序指執(zhí)行啊胶。
總結(jié)
相對于synchronized塊的代碼鎖甸各,volatile應(yīng)該是提供了一個輕量級的針對共享變量的鎖,當(dāng)我們在多個線程間使用共享變量進(jìn)行通信的時候需要考慮將共享變量用volatile來修飾焰坪。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末趣倾,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子某饰,更是在濱河造成了極大的恐慌儒恋,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,378評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件黔漂,死亡現(xiàn)場離奇詭異诫尽,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)炬守,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,970評論 3 399
  • 文/潘曉璐 我一進(jìn)店門牧嫉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人减途,你說我怎么就攤上這事酣藻。” “怎么了鳍置?”我有些...
    開封第一講書人閱讀 168,983評論 0 362
  • 文/不壞的土叔 我叫張陵辽剧,是天一觀的道長。 經(jīng)常有香客問我税产,道長怕轿,這世上最難降的妖魔是什么偷崩? 我笑而不...
    開封第一講書人閱讀 59,938評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮撤卢,結(jié)果婚禮上环凿,老公的妹妹穿的比我還像新娘。我一直安慰自己放吩,他們只是感情好智听,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,955評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著渡紫,像睡著了一般到推。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上惕澎,一...
    開封第一講書人閱讀 52,549評論 1 312
  • 那天莉测,我揣著相機(jī)與錄音,去河邊找鬼唧喉。 笑死捣卤,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的八孝。 我是一名探鬼主播董朝,決...
    沈念sama閱讀 41,063評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼干跛!你這毒婦竟也來了子姜?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,991評論 0 277
  • 序言:老撾萬榮一對情侶失蹤楼入,失蹤者是張志新(化名)和其女友劉穎哥捕,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體嘉熊,經(jīng)...
    沈念sama閱讀 46,522評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡遥赚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,604評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了记舆。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鸽捻。...
    茶點(diǎn)故事閱讀 40,742評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖泽腮,靈堂內(nèi)的尸體忽然破棺而出御蒲,到底是詐尸還是另有隱情,我是刑警寧澤诊赊,帶...
    沈念sama閱讀 36,413評論 5 351
  • 正文 年R本政府宣布厚满,位于F島的核電站,受9級特大地震影響碧磅,放射性物質(zhì)發(fā)生泄漏碘箍。R本人自食惡果不足惜遵馆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,094評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望丰榴。 院中可真熱鬧货邓,春花似錦、人聲如沸四濒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,572評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽盗蟆。三九已至戈二,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間喳资,已是汗流浹背觉吭。 一陣腳步聲響...
    開封第一講書人閱讀 33,671評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留仆邓,地道東北人鲜滩。 一個月前我還...
    沈念sama閱讀 49,159評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像节值,于是被迫代替她去往敵國和親绒北。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,747評論 2 361

推薦閱讀更多精彩內(nèi)容