從Java內(nèi)存模型出發(fā),結(jié)合并發(fā)編程中的原子性、可見性、有序性三個(gè)角度分析volatile所起的作用,并從匯編角度大致說了volatile的原理,說明了該關(guān)鍵字的應(yīng)用場(chǎng)景;在這補(bǔ)充一點(diǎn),分析下volatile是怎么在單例模式中避免雙檢鎖出現(xiàn)的問題的。
1、原子性:要實(shí)現(xiàn)原子性方式較多,可用synchronized、lock加鎖,AtomicInteger等,但volatile關(guān)鍵字是無法保證原子性的;
2、可見性:要實(shí)現(xiàn)可見性,也可用synchronized、lock,volatile關(guān)鍵字可用來保證可見性;
3、有序性:要避免指令重排序,synchronized、lock作用的代碼塊自然是有序執(zhí)行的,volatile關(guān)鍵字有效的禁止了指令重排序,實(shí)現(xiàn)了程序執(zhí)行的有序性;
雙重檢查鎖定(Double check locked)模式經(jīng)常會(huì)出現(xiàn)在一些框架源碼中,目的是為了延遲初始化變量。這個(gè)模式還可以用來創(chuàng)建單例。下面來看一個(gè) Spring 中雙重檢查鎖定的例子。
這個(gè)例子中需要將配置文件加載到 handlerMappings中,由于讀取資源比較耗時(shí),所以將動(dòng)作放到真正需要 handlerMappings的時(shí)候。我們可以看到 handlerMappings前面使用了volatile。有沒有想過為什么一定需要 volatile?雖然之前了解了雙重檢查鎖定模式的原理,但是卻忽略變量使用了 volatile。
下面我們就來看下這背后的原因。
想到延遲初始化一個(gè)變量,最簡單的例子就是取出變量進(jìn)行判斷。
這個(gè)例子在單線程環(huán)境可以正常運(yùn)行,但是在多線程環(huán)境就有可能會(huì)拋出空指針異常。為了防止這種情況,我們需要在該方法上使用 synchronized。這樣該方法在多線程環(huán)境就是安全的,但是這么做就會(huì)導(dǎo)致每次方法調(diào)用都需要獲取與釋放鎖,開銷很大。
深入分析可以得知只有在初始化的變量的需要真正加鎖,一旦初始化之后,直接返回對(duì)象即可。
所以我們可以將該方法改造以下的樣子。
這個(gè)方法首先判斷變量是否被初始化,沒有被初始化,再去獲取鎖。獲取鎖之后,再次判斷變量是否被初始化。第二次判斷目的在于有可能其他線程獲取過鎖,已經(jīng)初始化改變量。第二次檢查還未通過,才會(huì)真正初始化變量。
這個(gè)方法檢查判定兩次,并使用鎖,所以形象稱為雙重檢查鎖定模式。
這個(gè)方案縮小鎖的范圍,減少鎖的開銷,看起來很完美。然而這個(gè)方案有一些問題卻很容易被忽略。
這個(gè)被忽略的問題在于 Cache cache=new Cache()這行代碼并不是一個(gè)原子指令。使用 javap -c指令,可以快速查看字節(jié)碼。
從字節(jié)碼可以看到創(chuàng)建一個(gè)對(duì)象實(shí)例,可以分為三步:
分配對(duì)象內(nèi)存
調(diào)用構(gòu)造器方法,執(zhí)行初始化
將對(duì)象引用賦值給變量。
虛擬機(jī)實(shí)際運(yùn)行時(shí),以上指令可能發(fā)生重排序。以上代碼 2,3 可能發(fā)生重排序,但是并不會(huì)重排序 1 的順序。也就是說 1 這個(gè)指令都需要先執(zhí)行,因?yàn)?2,3 指令需要依托 1 指令執(zhí)行結(jié)果。
Java 語言規(guī)規(guī)定了線程執(zhí)行程序時(shí)需要遵守 intra-thread semantics。intra-thread semantics保證重排序不會(huì)改變單線程內(nèi)的程序執(zhí)行結(jié)果。這個(gè)重排序在沒有改變單線程程序的執(zhí)行結(jié)果的前提下,可以提高程序的執(zhí)行性能。
雖然重排序并不影響單線程內(nèi)的執(zhí)行結(jié)果,但是在多線程的環(huán)境就帶來一些問題。
上面錯(cuò)誤雙重檢查鎖定的示例代碼中,如果線程 1 獲取到鎖進(jìn)入創(chuàng)建對(duì)象實(shí)例,這個(gè)時(shí)候發(fā)生了指令重排序。當(dāng)線程1 執(zhí)行到 t3 時(shí)刻,線程 2 剛好進(jìn)入,由于此時(shí)對(duì)象已經(jīng)不為 Null,所以線程 2 可以自由訪問該對(duì)象。然后該對(duì)象還未初始化,所以線程 2 訪問時(shí)將會(huì)發(fā)生異常。
正確的雙重檢查鎖定模式需要需要使用 volatile。volatile主要包含兩個(gè)功能。
保證可見性。使用 volatile定義的變量,將會(huì)保證對(duì)所有線程的可見性。
禁止指令重排序優(yōu)化。
由于 volatile禁止對(duì)象創(chuàng)建時(shí)指令之間重排序,所以其他線程不會(huì)訪問到一個(gè)未初始化的對(duì)象,從而保證安全性。
注意,volatile禁止指令重排序在 JDK 5 之后才被修復(fù)
重新查看 Spring 中雙重檢查鎖定代碼。
可以看到方法內(nèi)部使用局部變量,首先將實(shí)例變量值賦值給該局部變量,然后再進(jìn)行判斷。最后內(nèi)容先寫入局部變量,然后再將局部變量賦值給實(shí)例變量。
使用局部變量相對(duì)于不使用局部變量,可以提高性能。主要是由于 volatile變量創(chuàng)建對(duì)象時(shí)需要禁止指令重排序,這就需要一些額外的操作。
對(duì)象的創(chuàng)建可能發(fā)生指令的重排序,使用 volatile可以禁止指令的重排序,保證多線程環(huán)境內(nèi)的系統(tǒng)安全。
歡迎大家一起交流,喜歡文章記得點(diǎn)個(gè)贊喲,感謝支持!
創(chuàng)新互聯(lián)www.cdcxhl.cn,專業(yè)提供香港、美國云服務(wù)器,動(dòng)態(tài)BGP最優(yōu)骨干路由自動(dòng)選擇,持續(xù)穩(wěn)定高效的網(wǎng)絡(luò)助力業(yè)務(wù)部署。公司持有工信部辦法的idc、isp許可證, 機(jī)房獨(dú)有T級(jí)流量清洗系統(tǒng)配攻擊溯源,準(zhǔn)確進(jìn)行流量調(diào)度,確保服務(wù)器高可用性。佳節(jié)活動(dòng)現(xiàn)已開啟,新人活動(dòng)云服務(wù)器買多久送多久。
網(wǎng)頁標(biāo)題:雙重檢查鎖單例模式為什么要用volatile關(guān)鍵字?-創(chuàng)新互聯(lián)
URL地址:http://jinyejixie.com/article18/eehdp.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站內(nèi)鏈、移動(dòng)網(wǎng)站建設(shè)、定制開發(fā)、ChatGPT、手機(jī)網(wǎng)站建設(shè)、自適應(yīng)網(wǎng)站
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)
猜你還喜歡下面的內(nèi)容