1. 程式人生 > 程式設計 >為什麼雙重檢查鎖模式需要 volatile ?

為什麼雙重檢查鎖模式需要 volatile ?

雙重檢查鎖定(Double check locked)模式經常會出現在一些框架原始碼中,目的是為了延遲初始化變數。這個模式還可以用來建立單例。下面來看一個 Spring 中雙重檢查鎖定的例子。

DCL.png

這個例子中需要將配置檔案載入到 handlerMappings中,由於讀取資源比較耗時,所以將動作放到真正需要 handlerMappings 的時候。我們可以看到 handlerMappings 前面使用了volatile 。有沒有想過為什麼一定需要 volatile?雖然之前瞭解了雙重檢查鎖定模式的原理,但是卻忽略變數使用了 volatile

下面我們就來看下這背後的原因。

錯誤的延遲初始化例子

想到延遲初始化一個變數,最簡單的例子就是取出變數進行判斷。

errorexample.png

這個例子在單執行緒環境交易正常執行,但是在多執行緒環境就有可能會丟擲空指標異常。為了防止這種情況,我們需要使用 synchronized 。這樣該方法在多執行緒環境就是安全的,但是這麼做就會導致每次呼叫該方法獲取與釋放鎖,開銷很大。

深入分析可以得知只有在初始化的變數的需要真正加鎖,一旦初始化之後,直接返回物件即可。

所以我們可以將該方法改造以下的樣子。

DCLerror.png

這個方法首先判斷變數是否被初始化,沒有被初始化,再去獲取鎖。獲取鎖之後,再次判斷變數是否被初始化。第二次判斷目的在於有可能其他執行緒獲取過鎖,已經初始化改變數。第二次檢查還未通過,才會真正初始化變數。

這個方法檢查判定兩次,並使用鎖,所以形象稱為雙重檢查鎖定模式。

這個方案縮小鎖的範圍,減少鎖的開銷,看起來很完美。然而這個方案有一些問題卻很容易被忽略。

new 例項背後的指令

這個被忽略的問題在於 Cache cache=new Cache() 這行程式碼並不是一個原子指令。使用 javap -c指令,可以快速檢視位元組碼。

	// 建立 Cache 物件例項,分配記憶體
       0: new           #5                  // class com/query/Cache
       // 複製棧頂地址,並再將其壓入棧頂
       3: dup
	// 呼叫構造器方法,初始化 Cache 物件
       4: invokespecial #6                  // Method "<init>":()V
	// 存入區域性方法變量表
       7: astore_1
複製程式碼

從位元組碼可以看到建立一個物件例項,可以分為三步:

  1. 分配物件記憶體
  2. 呼叫構造器方法,執行初始化
  3. 將物件引用賦值給變數。

虛擬機器器實際執行時,以上指令可能發生重排序。以上程式碼 2,3 可能發生重排序,但是並不會重排序 1 的順序。也就是說 1 這個指令都需要先執行,因為 2,3 指令需要依託 1 指令執行結果。

Java 語言規規定了執行緒執行程式時需要遵守 intra-thread semantics。**intra-thread semantics ** 保證重排序不會改變單執行緒內的程式執行結果。這個重排序在沒有改變單執行緒程式的執行結果的前提下,可以提高程式的執行效能。

雖然重排序並不影響單執行緒內的執行結果,但是在多執行緒的環境就帶來一些問題。

image.png

上面錯誤雙重檢查鎖定的示例程式碼中,如果執行緒 1 獲取到鎖進入建立物件例項,這個時候發生了指令重排序。當執行緒1 執行到 t3 時刻,執行緒 2 剛好進入,由於此時物件已經不為 Null,所以執行緒 2 可以自由訪問該物件。然後該物件還未初始化,所以執行緒 2 訪問時將會發生異常。

volatile 作用

正確的雙重檢查鎖定模式需要需要使用 volatilevolatile主要包含兩個功能。

  1. 保證可見性。使用 volatile 定義的變數,將會保證對所有執行緒的可見性。
  2. 禁止指令重排序優化。

由於 volatile 禁止物件建立時指令之間重排序,所以其他執行緒不會訪問到一個未初始化的物件,從而保證安全性。

注意,volatile禁止指令重排序在 JDK 5 之後才被修復

使用區域性變數優化效能

重新檢視 Spring 中雙重檢查鎖定程式碼。

DCL.png

可以看到方法內部使用區域性變數,首先將例項變數值賦值給該區域性變數,然後再進行判斷。最後內容先寫入區域性變數,然後再將區域性變數賦值給例項變數。

使用區域性變數相對於不使用區域性變數,可以提高效能。主要是由於 volatile 變數建立物件時需要禁止指令重排序,這就需要一些額外的操作。

總結

物件的建立可能發生指令的重排序,使用 volatile 可以禁止指令的重排序,保證多執行緒環境內的系統安全。

幫助檔案

雙重檢查鎖定與延遲初始化
有關“雙重檢查鎖定失效”的說明

其他平臺.png