摘要:注意,禁止指令重排序在之后才被修復(fù)使用局部變量優(yōu)化性能重新查看中雙重檢查鎖定代碼。幫助文檔雙重檢查鎖定與延遲初始化有關(guān)雙重檢查鎖定失效的說明
雙重檢查鎖定(Double check locked)模式經(jīng)常會出現(xiàn)在一些框架源碼中,目的是為了延遲初始化變量。這個模式還可以用來創(chuàng)建單例。下面來看一個 Spring 中雙重檢查鎖定的例子。
這個例子中需要將配置文件加載到 handlerMappings中,由于讀取資源比較耗時,所以將動作放到真正需要 handlerMappings 的時候。我們可以看到 handlerMappings 前面使用了volatile 。有沒有想過為什么一定需要 volatile?雖然之前了解了雙重檢查鎖定模式的原理,但是卻忽略變量使用了 volatile。
下面我們就來看下這背后的原因。
錯誤的延遲初始化例子想到延遲初始化一個變量,最簡單的例子就是取出變量進(jìn)行判斷。
這個例子在單線程環(huán)境交易正常運(yùn)行,但是在多線程環(huán)境就有可能會拋出空指針異常。為了防止這種情況,我們需要使用 synchronized 。這樣該方法在多線程環(huán)境就是安全的,但是這么做就會導(dǎo)致每次調(diào)用該方法獲取與釋放鎖,開銷很大。
深入分析可以得知只有在初始化的變量的需要真正加鎖,一旦初始化之后,直接返回對象即可。
所以我們可以將該方法改造以下的樣子。
這個方法首先判斷變量是否被初始化,沒有被初始化,再去獲取鎖。獲取鎖之后,再次判斷變量是否被初始化。第二次判斷目的在于有可能其他線程獲取過鎖,已經(jīng)初始化改變量。第二次檢查還未通過,才會真正初始化變量。
這個方法檢查判定兩次,并使用鎖,所以形象稱為雙重檢查鎖定模式。
這個方案縮小鎖的范圍,減少鎖的開銷,看起來很完美。然而這個方案有一些問題卻很容易被忽略。
new 實(shí)例背后的指令這個被忽略的問題在于 Cache cache=new Cache() 這行代碼并不是一個原子指令。使用 javap -c 指令,可以快速查看字節(jié)碼。
// 創(chuàng)建 Cache 對象實(shí)例,分配內(nèi)存 0: new #5 // class com/query/Cache // 復(fù)制棧頂?shù)刂罚⒃賹⑵鋲喝霔m? 3: dup // 調(diào)用構(gòu)造器方法,初始化 Cache 對象 4: invokespecial #6 // Method "":()V // 存入局部方法變量表 7: astore_1
從字節(jié)碼可以看到創(chuàng)建一個對象實(shí)例,可以分為三步:
分配對象內(nèi)存
調(diào)用構(gòu)造器方法,執(zhí)行初始化
將對象引用賦值給變量。
虛擬機(jī)實(shí)際運(yùn)行時,以上指令可能發(fā)生重排序。以上代碼 2,3 可能發(fā)生重排序,但是并不會重排序 1 的順序。也就是說 1 這個指令都需要先執(zhí)行,因?yàn)?2,3 指令需要依托 1 指令執(zhí)行結(jié)果。
Java 語言規(guī)規(guī)定了線程執(zhí)行程序時需要遵守 intra-thread semantics。intra-thread semantics 保證重排序不會改變單線程內(nèi)的程序執(zhí)行結(jié)果。這個重排序在沒有改變單線程程序的執(zhí)行結(jié)果的前提下,可以提高程序的執(zhí)行性能。
雖然重排序并不影響單線程內(nèi)的執(zhí)行結(jié)果,但是在多線程的環(huán)境就帶來一些問題。
上面錯誤雙重檢查鎖定的示例代碼中,如果線程 1 獲取到鎖進(jìn)入創(chuàng)建對象實(shí)例,這個時候發(fā)生了指令重排序。當(dāng)線程1 執(zhí)行到 t3 時刻,線程 2 剛好進(jìn)入,由于此時對象已經(jīng)不為 Null,所以線程 2 可以自由訪問該對象。然后該對象還未初始化,所以線程 2 訪問時將會發(fā)生異常。
volatile 作用正確的雙重檢查鎖定模式需要需要使用 volatile。volatile主要包含兩個功能。
保證可見性。使用 volatile 定義的變量,將會保證對所有線程的可見性。
禁止指令重排序優(yōu)化。
由于 volatile 禁止對象創(chuàng)建時指令之間重排序,所以其他線程不會訪問到一個未初始化的對象,從而保證安全性。
注意,volatile禁止指令重排序在 JDK 5 之后才被修復(fù)使用局部變量優(yōu)化性能
重新查看 Spring 中雙重檢查鎖定代碼。
可以看到方法內(nèi)部使用局部變量,首先將實(shí)例變量值賦值給該局部變量,然后再進(jìn)行判斷。最后內(nèi)容先寫入局部變量,然后再將局部變量賦值給實(shí)例變量。
使用局部變量相對于不使用局部變量,可以提高性能。主要是由于 volatile 變量創(chuàng)建對象時需要禁止指令重排序,這就需要一些額外的操作。
總結(jié)對象的創(chuàng)建可能發(fā)生指令的重排序,使用 volatile 可以禁止指令的重排序,保證多線程環(huán)境內(nèi)的系統(tǒng)安全。
幫助文檔雙重檢查鎖定與延遲初始化
有關(guān)“雙重檢查鎖定失效”的說明
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/76146.html
摘要:總結(jié)我們主要介紹到了以下幾種方式實(shí)現(xiàn)單例模式餓漢方式線程安全懶漢式非線程安全和關(guān)鍵字線程安全版本懶漢式雙重檢查加鎖版本枚舉方式參考設(shè)計模式中文版第二版設(shè)計模式深入理解單例模式我是一個以架構(gòu)師為年之內(nèi)目標(biāo)的小小白。 初遇設(shè)計模式在上個寒假,當(dāng)時把每個設(shè)計模式過了一遍,對設(shè)計模式有了一個最初級的了解。這個學(xué)期借了幾本設(shè)計模式的書籍看,聽了老師的設(shè)計模式課,對設(shè)計模式算是有個更進(jìn)一步的認(rèn)識。...
摘要:假設(shè)不發(fā)生編譯器重排和指令重排,線程修改了的值,但是修改以后,的值可能還沒有寫回到主存中,那么線程得到就是很自然的事了。同理,線程對于的賦值操作也可能沒有及時刷新到主存中。線程的最后操作與線程發(fā)現(xiàn)線程已經(jīng)結(jié)束同步。 很久沒更新文章了,對隔三差五過來刷更新的讀者說聲抱歉。 關(guān)于 Java 并發(fā)也算是寫了好幾篇文章了,本文將介紹一些比較基礎(chǔ)的內(nèi)容,注意,閱讀本文需要一定的并發(fā)基礎(chǔ)。 本文的...
摘要:概述在單例模式的懶漢模式中,方法通常是采用如下方式寫的這樣的寫法在多線程的情況下有可能造成不同的線程都實(shí)例化了一個對象出來,所以該寫法是線程不安全的。全部代碼這是普通模式這是同步模式這是雙重判否模式 概述 在單例模式的懶漢模式中,getInstance方法通常是采用如下方式寫的: public static STest getInstance(){ if(sTest==null...
摘要:基礎(chǔ)系列的與方法類初始化順序線程池如何彈性伸縮的幾個要點(diǎn)的緩存什么場景下使用阻塞隊(duì)列的使用及模式中的序本文主要介紹的相關(guān)知識。典型的使用場景,作為,采用來做信號通知不采用的容易出錯即模式,就是雙加鎖檢查模式。因而有了雙重檢測模式的應(yīng)用。 Java基礎(chǔ)系列 Java的hashcode與equals方法 Java類初始化順序 ThreadPoolExecutor線程池如何彈性伸縮 Has...
閱讀 1122·2021-09-22 15:32
閱讀 1722·2019-08-30 15:53
閱讀 3253·2019-08-30 15:53
閱讀 1404·2019-08-30 15:43
閱讀 453·2019-08-28 18:28
閱讀 2567·2019-08-26 18:18
閱讀 669·2019-08-26 13:58
閱讀 2528·2019-08-26 12:10