天天看點

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

文章目錄

  • ​​1.雙重檢查鎖定​​
  • ​​2.錯誤的延遲初始化例子​​
  • ​​3.new 執行個體背後的指令​​
  • ​​4.volatile作用​​
  • ​​5.使用局部變量優化性能​​
  • ​​6.總結​​

1.雙重檢查鎖定

雙重檢查鎖定(​

​Double check locked​

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

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

這個例子中需要将配置檔案加載到 ​

​handlerMappings​

​​中,由于讀取資源比較耗時,是以将動作放到真正需要 ​

​handlerMappings​

​​ 的時候。我們可以看到 ​

​handlerMappings​

​​ 前面使用了​

​volatile​

​​ 。有沒有想過為什麼一定需要 ​

​volatile​

​?雖然之前了解了雙重檢查鎖定模式的原理,但是卻忽略變量使用了 volatile。

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

2.錯誤的延遲初始化例子

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

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

這個例子在單線程環境交易正常運作,但是在多線程環境就有可能會抛出空指針異常。為了防止這種情況,我們需要使用 ​​

​synchronized​

​ 。這樣該方法在多線程環境就是安全的,但是這麼做就會導緻每次調用該方法擷取與釋放鎖,開銷很大。

深入分析可以得知隻有在初始化的變量的需要真正加鎖,一旦初始化之後,直接傳回對象即可。

是以我們可以将該方法改造以下的樣子。

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

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

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

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

3.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​

​ 保證重排序不會改變單線程内的程式執行結果。這個重排序在沒有改變單線程程式的執行結果的前提下,可以提高程式的執行性能。

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

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

上面錯誤雙重檢查鎖定的示例代碼中,如果線程 1 擷取到鎖進入建立對象執行個體,這個時候發生了指令重排序。當線程1 執行到 t3 時刻,線程 2 剛好進入,由于此時對象已經不為 Null,是以線程 2 可以自由通路該對象。然後該對象還未初始化,是以線程 2 通路時将會發生異常。

4.volatile作用

正确的雙重檢查鎖定模式需要需要使用​

​volatile​

​。volatile主要包含兩個功能。

  • 保證可見性。使用 volatile 定義的變量,将會保證對所有線程的可見性。
  • 禁止指令重排序優化。

由于 volatile 禁止對象建立時指令之間重排序,是以其他線程不會通路到一個未初始化的對象,進而保證安全性。

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

5.使用局部變量優化性能

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

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

可以看到方法内部使用局部變量,首先将執行個體變量值指派給該局部變量,然後再進行判斷。最後内容先寫入局部變量,然後再将局部變量指派給執行個體變量。

6.總結