雙重檢查鎖單例模式爲什麼要用volatile關鍵字?

前言

從Java內存模型出發,結合併發編程中的原子性、可見性、有序性三個角度分析volatile所起的作用,並從彙編角度大致說了volatile的原理,說明了該關鍵字的應用場景;在這補充一點,分析下volatile是怎麼在單例模式中避免雙檢鎖出現的問題的。

併發編程的3個條件

1、原子性:要實現原子性方式較多,可用synchronized、lock加鎖,AtomicInteger等,但volatile關鍵字是無法保證原子性的;

2、可見性:要實現可見性,也可用synchronized、lock,volatile關鍵字可用來保證可見性;

3、有序性:要避免指令重排序,synchronized、lock作用的代碼塊自然是有序執行的,volatile關鍵字有效的禁止了指令重排序,實現了程序執行的有序性;

雙重檢查鎖定模式

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

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

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

錯誤的延遲初始化例子

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

這個例子在單線程環境可以正常運行,但是在多線程環境就有可能會拋出空指針異常。爲了防止這種情況,我們需要在該方法上使用 synchronized。這樣該方法在多線程環境就是安全的,但是這麼做就會導致每次方法調用都需要獲取與釋放鎖,開銷很大。

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

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

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

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

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

new 實例背後的指令

這個被忽略的問題在於 Cache cache=new Cache()這行代碼並不是一個原子指令。使用 javap -c指令,可以快速查看字節碼。

從字節碼可以看到創建一個對象實例,可以分爲三步:

分配對象內存

調用構造器方法,執行初始化

將對象引用賦值給變量。

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

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

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

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

volatile 作用

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

保證可見性。使用 volatile定義的變量,將會保證對所有線程的可見性。

禁止指令重排序優化。

由於 volatile禁止對象創建時指令之間重排序,所以其他線程不會訪問到一個未初始化的對象,從而保證安全性。

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

使用局部變量優化性能

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

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

使用局部變量相對於不使用局部變量,可以提高性能。主要是由於 volatile變量創建對象時需要禁止指令重排序,這就需要一些額外的操作。

總結

對象的創建可能發生指令的重排序,使用 volatile可以禁止指令的重排序,保證多線程環境內的系統安全。

最後

歡迎大家一起交流,喜歡文章記得關注我點個喜歡喲,感謝支持!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章