溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

雙重檢查鎖單例模式為什么要用volatile關(guān)鍵字?

發(fā)布時間:2020-06-26 13:51:27 來源:網(wǎng)絡(luò) 閱讀:519 作者:架構(gòu)師追風(fēng) 欄目:編程語言

前言

從Java內(nèi)存模型出發(fā),結(jié)合并發(fā)編程中的原子性、可見性、有序性三個角度分析volatile所起的作用,并從匯編角度大致說了volatile的原理,說明了該關(guān)鍵字的應(yīng)用場景;在這補充一點,分析下volatile是怎么在單例模式中避免雙檢鎖出現(xiàn)的問題的。

并發(fā)編程的3個條件

1、原子性:要實現(xiàn)原子性方式較多,可用synchronized、lock加鎖,AtomicInteger等,但volatile關(guān)鍵字是無法保證原子性的;
2、可見性:要實現(xiàn)可見性,也可用synchronized、lock,volatile關(guān)鍵字可用來保證可見性;
3、有序性:要避免指令重排序,synchronized、lock作用的代碼塊自然是有序執(zhí)行的,volatile關(guān)鍵字有效的禁止了指令重排序,實現(xiàn)了程序執(zhí)行的有序性;

雙重檢查鎖定模式

雙重檢查鎖定(Double check locked)模式經(jīng)常會出現(xiàn)在一些框架源碼中,目的是為了延遲初始化變量。這個模式還可以用來創(chuàng)建單例。下面來看一個 Spring 中雙重檢查鎖定的例子。
雙重檢查鎖單例模式為什么要用volatile關(guān)鍵字?

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

錯誤的延遲初始化例子

想到延遲初始化一個變量,最簡單的例子就是取出變量進行判斷。
雙重檢查鎖單例模式為什么要用volatile關(guān)鍵字?

這個例子在單線程環(huán)境可以正常運行,但是在多線程環(huán)境就有可能會拋出空指針異常。為了防止這種情況,我們需要在該方法上使用 synchronized。這樣該方法在多線程環(huán)境就是安全的,但是這么做就會導(dǎo)致每次方法調(diào)用都需要獲取與釋放鎖,開銷很大。
深入分析可以得知只有在初始化的變量的需要真正加鎖,一旦初始化之后,直接返回對象即可。
所以我們可以將該方法改造以下的樣子。
雙重檢查鎖單例模式為什么要用volatile關(guān)鍵字?

這個方法首先判斷變量是否被初始化,沒有被初始化,再去獲取鎖。獲取鎖之后,再次判斷變量是否被初始化。第二次判斷目的在于有可能其他線程獲取過鎖,已經(jīng)初始化改變量。第二次檢查還未通過,才會真正初始化變量。
這個方法檢查判定兩次,并使用鎖,所以形象稱為雙重檢查鎖定模式。
這個方案縮小鎖的范圍,減少鎖的開銷,看起來很完美。然而這個方案有一些問題卻很容易被忽略。

new 實例背后的指令

這個被忽略的問題在于 Cache cache=new Cache()這行代碼并不是一個原子指令。使用 javap -c指令,可以快速查看字節(jié)碼。
雙重檢查鎖單例模式為什么要用volatile關(guān)鍵字?

從字節(jié)碼可以看到創(chuàng)建一個對象實例,可以分為三步:
分配對象內(nèi)存
調(diào)用構(gòu)造器方法,執(zhí)行初始化
將對象引用賦值給變量。
虛擬機實際運行時,以上指令可能發(fā)生重排序。以上代碼 2,3 可能發(fā)生重排序,但是并不會重排序 1 的順序。也就是說 1 這個指令都需要先執(zhí)行,因為 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)境就帶來一些問題。
雙重檢查鎖單例模式為什么要用volatile關(guān)鍵字?

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

volatile 作用

正確的雙重檢查鎖定模式需要需要使用 volatile。volatile主要包含兩個功能。
保證可見性。使用 volatile定義的變量,將會保證對所有線程的可見性。
禁止指令重排序優(yōu)化。
由于 volatile禁止對象創(chuàng)建時指令之間重排序,所以其他線程不會訪問到一個未初始化的對象,從而保證安全性。
注意,volatile禁止指令重排序在 JDK 5 之后才被修復(fù)

使用局部變量優(yōu)化性能

重新查看 Spring 中雙重檢查鎖定代碼。
雙重檢查鎖單例模式為什么要用volatile關(guān)鍵字?

可以看到方法內(nèi)部使用局部變量,首先將實例變量值賦值給該局部變量,然后再進行判斷。最后內(nèi)容先寫入局部變量,然后再將局部變量賦值給實例變量。
使用局部變量相對于不使用局部變量,可以提高性能。主要是由于 volatile變量創(chuàng)建對象時需要禁止指令重排序,這就需要一些額外的操作。

總結(jié)

對象的創(chuàng)建可能發(fā)生指令的重排序,使用 volatile可以禁止指令的重排序,保證多線程環(huán)境內(nèi)的系統(tǒng)安全。

最后

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

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI