從Java內存模型講雙重檢查鎖原理

一、什么是雙重檢查鎖

雙重檢查鎖(Double-Check Locking),顧名思義,通過兩次檢查,并基于加鎖機制,實現某個功能。

要理解什么是雙重檢查鎖,我們從常見的單例模式說起。看第一個例子:


未加鎖的單例模式

上圖中的單例模式一看就知道存在線程問題,如果兩個線程:線程A和線程B,同時訪問該類,線程A訪問到第6行,在還沒有實例化完成的時候,線程B訪問到第5行,此時也會判斷到instance=null,同樣會執行實例化的代碼,那么線程A和線程B就都會創建一個Singleton實例。

怎么解決上面出現的多線程并發訪問導致的問題呢?

加鎖!大家都知道,加鎖是解決并發訪問的方案,于是對代碼進行修改,修改后如下:


同步后的單例模式

基于鎖的相關特性,可以保證線程A和線程B對getInstance()方法的互斥性和原子性,線程A獲取到鎖,訪問getInstance方法,判斷到instance=null,然后創建instance對象實例,完成后釋放鎖;線程B獲取到鎖,進入getInstance方法,判斷到instance!=null,直接返回instance對象。這樣,保證了線程安全性。

但是,加鎖是會產生性能開銷的,如果getInstance方法被多個線程頻繁調用,將會導致程序性能下降。為了解決加鎖導致的性能開銷,想到了通過延遲加鎖,采用塊級鎖的方法提高性能降低開銷,代碼示例如下:


延遲加鎖實現單例模式

此時,線程A和線程B同時訪問getInstance方法,線程A和線程B同時在第5行判斷到instance=null,然后獲取到鎖,執行實例化代碼后釋放鎖,線程B也會獲取到鎖執行實例化代碼,所以這個并沒有解決線程同步的問題。那么,在線程B獲取到鎖之后,再判斷一次instance對象是否為null呢?


雙重檢查鎖實現單例模式

線程A和線程B同時訪問到第5行,然后線程A獲取到鎖,判斷到instance=null,執行實例化代碼然后釋放鎖;線程B獲取到鎖,判斷到instance!=null,直接返回instance對象;線程C在進入第5行后判斷到instance!=null,直接返回。這樣就減少了鎖的開銷,提升了性能。

此時看上去雙重檢查鎖機制很完美,創建單例實例沒有問題。但是,從JAVA內存模型來講,這其實是存在問題的,問題就在于,線程C在進入第5行后判斷到的不為null的instance對象,可能還沒有初始化完成!這就要從執行指令的重排序講起。

二、什么是重排序機制

為了提高程序執行性能,編譯器和處理器會對指令的處理過程重排序。重排序分為3種類型:

1、編譯器優化的重排序。在不改變單線程程序語義的前提下,可以重新安排語句的執行順序;

2、指令級并行的重排序。現代處理器采用了指令級并行技術將多條指令重疊執行,如果不存在數據依賴性,處理器可以改變代碼語句對應指令的執行順序;

3、內存系統的重排序。由于處理器使用緩存和讀/寫緩沖區,使得加載和存儲操作看上去可能是在亂序執行。

從Java源代碼到最終實際執行的指令序列,會經過如下步驟:


從源代碼到最終實際執行指令步驟

重排序可能會出現內存可見性問題,JMM通過內存屏障禁止了一些情況下的處理器重排序,保證了內存可見性問題。同時,基于as-if-serial語義,不管怎么重排序,單線程執行結果不能被改變。

對于具有數據依賴性的多個代碼指令,JMM會禁止重排序。但是對于不具有數據依賴性的代碼指令,JMM允許執行順序的重排序,只要保證單線程內執行結果一致即可。見下面的代碼:


重排序

第3行由于使用了第1行的執行結果和第2行的執行結果,因此與第1行和第2行存在數據依賴性,因此JMM保證第1行和第2行肯定會在第3行之前執行,不允許重排序。但是第1行和第2行不存在數據依賴性,是允許重排序的,他們之間只要保證執行結果的可見性即可。

三、回到雙重檢查鎖上來

為什么說線程C在第5行判斷到的instance!=null,可能是還未被初始化完成的對象實例呢?因為JMM在創建對象時會分為如下三步:


JMM創建對象過程

從上面可以看到,第2行和第3行執行需要依賴于第1行的執行結果,分配內存空間,存在數據依賴性。

但是第2行和第3行卻不存在數據依賴性,這里可能會發生重排序!

也就是說第3行可能會優先于第2行執行,先設置instance指向內存地址,此時instance就!=null;然后再初始化對象。如果初始化對象耗時較長,在還沒有初始化完成時,線程C訪問getInstance方法,在第5行會判斷到instance!=null,直接返回未初始化完成的instance實例對象。

怎么來解決這個問題?這又要說到JMM的happens-before規則。

四、什么是happens-before規則

從JDK5開始,Java使用新的JSR-133內存模型,使用happens-before的概念來闡述操作之間的內存可見性。在JMM中,如果一個操作的結果需要對另一個結果可見,那么這兩個操作之間必須存在happens-before關系。這兩個操作既可以是一個線程內,也可以是不同線程。

happens-before規則具有如下語義:

1、一個線程中的每個操作,happens-before于該線程中的任意后續操作;

2、對一個鎖的解鎖,happens-before于隨后對這個鎖的加鎖;

3、對一個 volatile修飾的變量的寫,happens-before于任意后續對該變量的讀;

4、如果A happens-before B,B happens-before C,那么A happens-before C。

我們看第3點,如果用volatile修飾instance,那么是不是可以保證程序按照我們的想法執行呢?答案是的!

五、什么是volatile

要講volatile,我們從一個例子開始講起。


volatile示例1

上面的代碼執行后,按照我們的理解應該是線程run方法里面應該最后跳出循環。但是事實上并不是,這是因為基于JMM,每個線程具有自己的工作內存空間,臨界區變量flag處于主內存空間中,每個線程從主內存空間中讀flag,在工作空間中寫flag,寫完后再刷入到主內存空間。這是JMM的基于內存模型,也正是因為這個模型,才會導致多線程的一致性問題。因為如果有多個線程同時從主內存中讀取臨界區變量,然后修改,就會保證數據的不一致性,所以只能通過加鎖來保證主內存區的訪問的互斥性。

上面的例子,主線程從主內存區讀取到flag=false,同時子線程也從主內存區讀取到flag=false,開始執行循環操作。即使主線程修改了flag=true,但是卻不能改變子線程工作內存區域的flag的值。要解決這個問題,一是通過加鎖實現可見性,二是通過volatile。

volatile的語義是:

1、保證線程之間的可見性,基于JMM內存模型,經過volatile修飾的變量,如果一個線程修改了該變量的值,會立刻刷新到主內存區域,此時基于happens-before規則,其他線程要讀該變量的值,必須要寫完之后。事實上,基于內存模型,主要是因為在修改了該變量的值后,內存模型會通知其他讀取了該值得線程,將值設置為無效,要使用該變量的值,必須從主內存區域重新讀取。從而保證了可見性。上例中如果用volatile修改flag后,子線程在遍歷時就會重新讀取flag的值。

2、volatile修飾的變量禁止重排序。

六、再次回到雙重檢查鎖

基于volatile修飾的變量禁止重排序的特性,以及volatile的happens-before規則,我們可以在上面的單例代碼中通過volatile修改,來最終實現線程安全的單例模式。


線程安全的單例模式



更多技術分享,歡迎訪問個人站點:技術驛站

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,119評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,382評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,038評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,853評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,616評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,112評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,192評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,355評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,869評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,727評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,928評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,467評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,165評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,570評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,813評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,585評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,892評論 2 372

推薦閱讀更多精彩內容