Java雙重檢查加鎖單例模式的詳解
什么是DCL
DCL(Double-checked locking)被設(shè)計成支持延遲加載,當(dāng)一個對象直到真正需要時才實例化:
class SomeClass {
private Resource resource = null;
public Resource getResource() {
if (resource == null)
resource = new Resource();
return resource;
}
}
為什么需要推遲初始化?可能創(chuàng)建對象是一個昂貴的操作,有時在已知的運行中可能根本就不會去調(diào)用它,這種情況下能避免創(chuàng)建一個不需要的對象。延遲初始化能讓程序啟動更快。但是在多線程環(huán)境下,可能會被初始化兩次,所以需要把getResource()方法聲明為synchronized。不幸的是,synchronized方法比非synchronized方法慢100倍左右,延遲初始化的初衷是為了提高效率,但是加上synchronized后,提高了啟動速度,卻大幅下降了執(zhí)行時速度,這看起來并不是一樁好買賣。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)條件??雌饋硎且粋€聰明的優(yōu)化--但它卻不能保證正常工作。為提高計算機系統(tǒng)性能,編譯器、處理器、緩存會對程序指令和數(shù)據(jù)進行重排序,而對象初始化操作并不是一個原子操作(可能會被重排序);因此可能存在這種情況:一個線程正在構(gòu)造對象過程中,另一個線程檢查時看見了resource的引用為非null。對象被非安全發(fā)布(逸出)。
根據(jù)Java內(nèi)存模型,synchronized的語義不僅僅是在同一個信號上的互斥(mutex),也包含線程和主存之間數(shù)據(jù)交互的同步,它確保在多處理器、多線程下對內(nèi)存能有可預(yù)見的一致性視圖。獲取或釋放鎖會觸發(fā)一次內(nèi)存屏障(memory barrier)--強迫線程本地內(nèi)存和主存同步。當(dāng)一個線程退出一個synchronized block時,觸發(fā)一次寫屏障(write barrier )--在釋放鎖前必須把所有在這個同步塊里修改過的變量值刷新到主存;同樣,進入一個synchronized block時,觸發(fā)一次讀屏障(read barrier)--讓本地內(nèi)存失效,必須從主存中重新獲取在這個同步塊中將要引用的所有變量的值。正確使用同步能保證一個線程能以可預(yù)見的方式看到另一個線程的結(jié)果,線程對同步塊的操作就像是原子的?!罢_使用”的含義是:必須是在同一個鎖上同步。
DCL是怎么失效的
了解了JMM后,再來看看DCL是怎么失效的。DCL依賴于一個非同步的resource字段,看起來無害,實則不然。假如線程A進入了synchronized block,正在執(zhí)行resource = new Resource();此時線程B進入 getResource()??紤]到對象初始化在內(nèi)存上的影響:為new對象分配內(nèi)存;調(diào)用構(gòu)造方法,初始化對象的成員變量;把新創(chuàng)建好對象的引用賦值給SomeClass的resource字段。然而線程B沒有進入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進入檢查發(fā)現(xiàn)resource不是null,跳過synchronized block返回一個未構(gòu)造完成的對象!顯而易見,結(jié)果不是預(yù)期的也不是想要的。
下面代碼是一個試圖修復(fù)DCL的加強版,遺憾的是它仍然不能保證正常工作。
// (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)造放在一個內(nèi)部的同步塊,又用了一個局部變量h來先接收初始化完成后的引用,直覺就是當(dāng)這個內(nèi)部的同步塊退出時,應(yīng)該會觸發(fā)一次內(nèi)存屏障,能阻止對初始化Helper對象和給Foo的helper字段賦值的兩個操作重排序。不幸的是,直覺是完全錯誤的,對同步規(guī)則理解得不對。對于monitorexit規(guī)則(即,釋放同步),監(jiān)視器被釋放之前必須執(zhí)行monitorexit之前的動作。然而,沒有規(guī)定說monitorexit后的操作,不能在監(jiān)視器釋放前執(zhí)行。編譯器把賦值語句helper = h;移動到內(nèi)部同步塊之前是完全合理合法的,在這種情況下,我們又重新回到了以前。許多處理器提供執(zhí)行這種單向內(nèi)存屏障指令。改變語義要求釋放鎖是一個完整的內(nèi)存屏障會有性能損失。然而即使初始化時有一個完整的內(nèi)存屏障,也不能保證,在一些系統(tǒng)上,保證線程能看到helper的屬性字段的值為非null也需要同樣的內(nèi)存屏障。因為處理器有自己的本地緩存拷貝,某些處理器在執(zhí)行緩存一致性指令前,即使其他的處理器使用內(nèi)存屏障強制把最新值寫入主存,該處理器讀到的還是本地緩存拷貝的舊值。
關(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)境下單例模式的實現(xiàn)。
多線程環(huán)境下單例的實現(xiàn)
第一種,同步方法(synchronized)
優(yōu)點:所有情況下都能正常工作,延遲初始化;
缺點:同步嚴(yán)重?fù)p耗了性能,因為只有第一次實例化時才需要同步。
不推薦,絕大部分情況是沒必要延遲初始化的,不如采用急切實例化(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塊做初始化,如下定義一個私有的靜態(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...
}
第三種,急切實例化(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及引用對象無效,因為64位的基本類型的賦值操作不是原子的。利用場景有限。
// 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字段都會把工作內(nèi)存及時刷新到主存,每次讀都會從主存獲取數(shù)據(jù),因為要和主存交換數(shù)據(jù),volatile的頻繁讀寫會占用數(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)本身是線程安全的,不需要同步,單例實現(xiàn)起來最簡單。比如Helper是一個不可變類型,只用用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后使用,因為新內(nèi)存模型對final和volatile語義進行了加強。還有一個問題就是明確什么是不可變對象,如果對不可變對象含義不確定,請不要使用,另外當(dāng)前是不可變對象不能保證將來此類一直是不可變對象(代碼總是在不斷修改),慎用!
需要使用單例時,慎用延遲初始化,優(yōu)先考慮急切實例化(簡單優(yōu)雅,不易出錯)
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,謝謝大家對腳本之家的支持。如果你想了解更多相關(guān)內(nèi)容請查看下面相關(guān)鏈接
相關(guān)文章
Spring?Boot?快速使用?HikariCP?連接池配置詳解
Spring Boot 2.x 將其作為默認(rèn)的連接池組件,項目中添加 spring-boot-starter-jdbc 或 spring-boot-starter-data-jpa 模塊后,HikariCP 依賴會被自動引入,這篇文章主要介紹了Spring?Boot使用HikariCP連接池配置詳解,需要的朋友可以參考下2023-06-06
Spring MVC 請求參數(shù)綁定實現(xiàn)方式
Spring MVC 是一個用于構(gòu)建 Web 應(yīng)用程序的框架,它提供了一種方便的方式來處理 HTTP 請求和響應(yīng),Spring MVC 提供了多種方式來實現(xiàn)請求參數(shù)綁定,本文結(jié)合實例代碼給大家介紹的非常詳細(xì),需要的朋友跟隨小編一起看看吧2023-09-09
詳解Spring Cloud Config采用Git存儲時兩種常用的配置策略
這篇文章主要介紹了詳解Spring Cloud Config采用Git存儲時兩種常用的配置策略,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-07-07
java(swing)+ mysql實現(xiàn)學(xué)生信息管理系統(tǒng)源碼
這篇文章主要分享了java mysql實現(xiàn)學(xué)生信息管理系統(tǒng)的源碼,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-11-11
Spring Cloud Feign接口返回流的實現(xiàn)
這篇文章主要介紹了Spring Cloud Feign接口返回流的實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10

