Java雙重檢查加鎖單例模式的詳解
什么是DCL
DCL(Double-checked locking)被設(shè)計(jì)成支持延遲加載,當(dāng)一個(gè)對象直到真正需要時(shí)才實(shí)例化:
class SomeClass { private Resource resource = null; public Resource getResource() { if (resource == null) resource = new Resource(); return resource; } }
為什么需要推遲初始化?可能創(chuàng)建對象是一個(gè)昂貴的操作,有時(shí)在已知的運(yùn)行中可能根本就不會(huì)去調(diào)用它,這種情況下能避免創(chuàng)建一個(gè)不需要的對象。延遲初始化能讓程序啟動(dòng)更快。但是在多線程環(huán)境下,可能會(huì)被初始化兩次,所以需要把getResource()方法聲明為synchronized。不幸的是,synchronized方法比非synchronized方法慢100倍左右,延遲初始化的初衷是為了提高效率,但是加上synchronized后,提高了啟動(dòng)速度,卻大幅下降了執(zhí)行時(shí)速度,這看起來并不是一樁好買賣。DCL看起來是最好的:
class SomeClass { private Resource resource = null; public Resource getResource() { if (resource == null) { synchronized(this) { if (resource == null) resource = new Resource(); } } return resource; } }
延遲了初始化,又避免了競態(tài)條件??雌饋硎且粋€(gè)聰明的優(yōu)化--但它卻不能保證正常工作。為提高計(jì)算機(jī)系統(tǒng)性能,編譯器、處理器、緩存會(huì)對程序指令和數(shù)據(jù)進(jìn)行重排序,而對象初始化操作并不是一個(gè)原子操作(可能會(huì)被重排序);因此可能存在這種情況:一個(gè)線程正在構(gòu)造對象過程中,另一個(gè)線程檢查時(shí)看見了resource的引用為非null。對象被非安全發(fā)布(逸出)。
根據(jù)Java內(nèi)存模型,synchronized的語義不僅僅是在同一個(gè)信號(hào)上的互斥(mutex),也包含線程和主存之間數(shù)據(jù)交互的同步,它確保在多處理器、多線程下對內(nèi)存能有可預(yù)見的一致性視圖。獲取或釋放鎖會(huì)觸發(fā)一次內(nèi)存屏障(memory barrier)--強(qiáng)迫線程本地內(nèi)存和主存同步。當(dāng)一個(gè)線程退出一個(gè)synchronized block時(shí),觸發(fā)一次寫屏障(write barrier )--在釋放鎖前必須把所有在這個(gè)同步塊里修改過的變量值刷新到主存;同樣,進(jìn)入一個(gè)synchronized block時(shí),觸發(fā)一次讀屏障(read barrier)--讓本地內(nèi)存失效,必須從主存中重新獲取在這個(gè)同步塊中將要引用的所有變量的值。正確使用同步能保證一個(gè)線程能以可預(yù)見的方式看到另一個(gè)線程的結(jié)果,線程對同步塊的操作就像是原子的?!罢_使用”的含義是:必須是在同一個(gè)鎖上同步。
DCL是怎么失效的
了解了JMM后,再來看看DCL是怎么失效的。DCL依賴于一個(gè)非同步的resource字段,看起來無害,實(shí)則不然。假如線程A進(jìn)入了synchronized block,正在執(zhí)行resource = new Resource();此時(shí)線程B進(jìn)入 getResource()??紤]到對象初始化在內(nèi)存上的影響:為new對象分配內(nèi)存;調(diào)用構(gòu)造方法,初始化對象的成員變量;把新創(chuàng)建好對象的引用賦值給SomeClass的resource字段。然而線程B沒有進(jìn)入synchronized block,卻可能以不同于線程A執(zhí)行的順序看到上述內(nèi)存操作。B看到的可能是如下順序(指令重排序):分配內(nèi)存,把對象引用賦值給SomeClass的resource字段,調(diào)用構(gòu)造器。當(dāng)內(nèi)存已經(jīng)分配好,A線程把SomeClass的resource字段設(shè)值完成后,線程B進(jìn)入檢查發(fā)現(xiàn)resource不是null,跳過synchronized block返回一個(gè)未構(gòu)造完成的對象!顯而易見,結(jié)果不是預(yù)期的也不是想要的。
下面代碼是一個(gè)試圖修復(fù)DCL的加強(qiáng)版,遺憾的是它仍然不能保證正常工作。
// (Still) Broken multithreaded version // "Double-Checked Locking" idiom class Foo { private Helper helper = null; public Helper getHelper() { if (helper == null) { Helper h; synchronized (this) { h = helper; if (h == null) synchronized (this) { h = new Helper(); } // release inner synchronization lock helper = h; } } return helper; } // other functions and members... }
這段代碼把Helper對象的構(gòu)造放在一個(gè)內(nèi)部的同步塊,又用了一個(gè)局部變量h來先接收初始化完成后的引用,直覺就是當(dāng)這個(gè)內(nèi)部的同步塊退出時(shí),應(yīng)該會(huì)觸發(fā)一次內(nèi)存屏障,能阻止對初始化Helper對象和給Foo的helper字段賦值的兩個(gè)操作重排序。不幸的是,直覺是完全錯(cuò)誤的,對同步規(guī)則理解得不對。對于monitorexit規(guī)則(即,釋放同步),監(jiān)視器被釋放之前必須執(zhí)行monitorexit之前的動(dòng)作。然而,沒有規(guī)定說monitorexit后的操作,不能在監(jiān)視器釋放前執(zhí)行。編譯器把賦值語句helper = h;移動(dòng)到內(nèi)部同步塊之前是完全合理合法的,在這種情況下,我們又重新回到了以前。許多處理器提供執(zhí)行這種單向內(nèi)存屏障指令。改變語義要求釋放鎖是一個(gè)完整的內(nèi)存屏障會(huì)有性能損失。然而即使初始化時(shí)有一個(gè)完整的內(nèi)存屏障,也不能保證,在一些系統(tǒng)上,保證線程能看到helper的屬性字段的值為非null也需要同樣的內(nèi)存屏障。因?yàn)樘幚砥饔凶约旱谋镜鼐彺婵截?,某些處理器在?zhí)行緩存一致性指令前,即使其他的處理器使用內(nèi)存屏障強(qiáng)制把最新值寫入主存,該處理器讀到的還是本地緩存拷貝的舊值。
關(guān)于重排序(reorder)有3種來源:編譯器、處理器、內(nèi)存系統(tǒng)。承諾“write-once, run-anywhere concurrent applications in Java” 的Java是接受處理器和內(nèi)存系統(tǒng)為優(yōu)化而重排序的,所以DCL單例模式?jīng)]有完美的解決方案,在多線程下編程要異常小心。下面討論多線程環(huán)境下單例模式的實(shí)現(xiàn)。
多線程環(huán)境下單例的實(shí)現(xiàn)
第一種,同步方法(synchronized)
優(yōu)點(diǎn):所有情況下都能正常工作,延遲初始化;
缺點(diǎn):同步嚴(yán)重?fù)p耗了性能,因?yàn)橹挥械谝淮螌?shí)例化時(shí)才需要同步。
不推薦,絕大部分情況是沒必要延遲初始化的,不如采用急切實(shí)例化(eager initialization)
// Correct multithreaded version class Foo { private Helper helper = null; public synchronized Helper getHelper() { if (helper == null) helper = new Helper(); return helper; } // other functions and members... }
第二種,使用IODH(Initialization On Demand Holder)
利用static塊做初始化,如下定義一個(gè)私有的靜態(tài)類去做初始化,或者直接在靜態(tài)塊代碼中去做初始化,能保證對象被正確構(gòu)造前對所有線程不可見。
class Foo { private static class HelperSingleton { public static Helper singleton = new Helper(); } public Helper getHelper() { return HelperSingleton.singleton; } // other functions and members... }
第三種,急切實(shí)例化(eager initialization)
class Foo { public static final Helper singleton = new Helper(); // other functions and members... } class Foo { private static final Helper singleton = new Helper(); public Helper getHelper() { return singleton; } // other functions and members... }
第四種,枚舉單例
public enum SingletonClass { INSTANCE; // other functions... }
上面4種方式在所有情況下都能保證正常工作
第五種,只對32位基本類型的值有效
缺陷:對64位的long和double及引用對象無效,因?yàn)?4位的基本類型的賦值操作不是原子的。利用場景有限。
// Lazy initialization 32-bit primitives // Thread-safe if computeHashCode is idempotent class Foo { private int cachedHashCode = 0; public int hashCode() { int h = cachedHashCode; if (h == 0) { h = computeHashCode(); cachedHashCode = h; } return h; } // other functions and members... }
第六種,DCL加上volatile語義
舊內(nèi)存模型(在JDK1.5發(fā)行之前)下失效,只能在JDK1.5后使用。
另外不推薦次方法,多核處理器下線程每次寫volatile字段都會(huì)把工作內(nèi)存及時(shí)刷新到主存,每次讀都會(huì)從主存獲取數(shù)據(jù),因?yàn)橐椭鞔娼粨Q數(shù)據(jù),volatile的頻繁讀寫會(huì)占用數(shù)據(jù)總線資源。
// Works with acquire/release semantics for volatile // Broken under current semantics for volatile class Foo { private volatile Helper helper = null; public Helper getHelper() { Helper h = helper; if (helper == null) {// First check (no locking) synchronized (this) { h = helper; if (helper == null) helper = h = new Helper(); } } return helper; } }
第七種,不可變對象的單例
對于不可變對象(immutable object)本身是線程安全的,不需要同步,單例實(shí)現(xiàn)起來最簡單。比如Helper是一個(gè)不可變類型,只用用final修飾singleton字段就行:
class Foo { private final Helper singleton = new Helper(); public Helper getHelper() { return singleton; } // other functions and members... }
缺陷:舊內(nèi)存模型(在JDK1.5發(fā)行之前)下失效,只能在JDK1.5后使用,因?yàn)樾聝?nèi)存模型對final和volatile語義進(jìn)行了加強(qiáng)。還有一個(gè)問題就是明確什么是不可變對象,如果對不可變對象含義不確定,請不要使用,另外當(dāng)前是不可變對象不能保證將來此類一直是不可變對象(代碼總是在不斷修改),慎用!
需要使用單例時(shí),慎用延遲初始化,優(yōu)先考慮急切實(shí)例化(簡單優(yōu)雅,不易出錯(cuò))
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對腳本之家的支持。如果你想了解更多相關(guān)內(nèi)容請查看下面相關(guān)鏈接
相關(guān)文章
Java8學(xué)習(xí)教程之lambda表達(dá)式語法介紹
眾所周知lambda表達(dá)式是JAVA8中提供的一種新的特性,它支持Java也能進(jìn)行簡單的“函數(shù)式編程”。 下面這篇文章主要給大家介紹了關(guān)于Java8學(xué)習(xí)教程之lambda表達(dá)式語法的相關(guān)資料,需要的朋友可以參考下。2017-09-09idea顯示springboot多服務(wù)啟動(dòng)界面service操作
這篇文章主要介紹了idea顯示springboot多服務(wù)啟動(dòng)界面service操作,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-09-09jar包運(yùn)行一段時(shí)間后莫名其妙掛掉線上問題及處理方案
這篇文章主要介紹了jar包運(yùn)行一段時(shí)間后莫名其妙掛掉線上問題及處理方案,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-09-09Spring?代理?Bean?獲取不到原始?Bean?對象注解解決方法
這篇文章主要介紹了Spring?代理?Bean?獲取不到原始?Bean?對象注解解決方法,文章圍繞主題相關(guān)資料展開詳細(xì)介紹,需要的小伙伴可以參考一下2022-04-04關(guān)于java入門與java開發(fā)環(huán)境配置詳細(xì)教程
這篇文章主要介紹了關(guān)于java入門與java開發(fā)環(huán)境配置詳細(xì)教程,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03偵聽消息隊(duì)列的Message Listener類示例詳解
Spring AMQP 是基于 Spring 框架的AMQP消息解決方案,提供模板化的發(fā)送和接收消息的抽象層,提供基于消息驅(qū)動(dòng)的 POJO的消息監(jiān)聽等,簡化了我們對于RabbitMQ相關(guān)程序的開發(fā),本文給大家介紹偵聽消息隊(duì)列的Message Listener類,感興趣的朋友一起看看吧2023-12-12IntelliJ IDEA失焦自動(dòng)重啟服務(wù)的解決方法
在使用 IntelliJ IDEA運(yùn)行 SpringBoot 項(xiàng)目時(shí),你可能會(huì)遇到一個(gè)令人困擾的問題,一旦你的鼠標(biāo)指針離開當(dāng)前IDE窗口,點(diǎn)擊其他位置時(shí), IDE 窗口會(huì)失去焦點(diǎn),你的 SpringBoot 服務(wù)就會(huì)自動(dòng)重啟,所以本文給大家介紹了IntelliJ IDEA失焦自動(dòng)重啟服務(wù)的解決方法2023-10-10項(xiàng)目打包成jar后包無法讀取src/main/resources下文件的解決
本文主要介紹了項(xiàng)目打包成jar后包無法讀取src/main/resources下文件的解決,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04