概念
單例模式是一種常用的軟件設(shè)計模式缀踪。在應(yīng)用這個模式時滚局,單例對象的類必須保證只有一個實例存在工猜。本文就從單例模式的兩種構(gòu)建方式來了解一下單例。以下會給出多種單例的實現(xiàn)币狠,有正確的、也有存在缺陷的砾层。最后會總結(jié)各個方式優(yōu)缺點漩绵。
分類
- 餓漢式單例模式:指全局的單例實例在類加載時就主動創(chuàng)建實例。
- 懶漢式單例模式:指全局的單例實例在第一次被使用時才創(chuàng)建實例肛炮,不使用時不創(chuàng)建實例止吐。
實現(xiàn)方式
1。餓漢式:(記為 實現(xiàn)-1)
形象的描述就是“直接”侨糟,想象一下一名餓漢在吃東西時的樣子碍扔。食物到面前就開吃,簡單粗暴秕重。而代碼中的體現(xiàn)就是一被加載就構(gòu)建不同。實現(xiàn)起來也是簡單粗暴,沒有缺陷溶耘,唯一的不足就是耗費資源二拐。因為就算這個單例沒被使用到,它也會被實例化凳兵,占用內(nèi)存百新。
示例代碼
public class Singleton {
private static Singleton instance= new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
餓漢式的單例模式上面代碼就已經(jīng)實現(xiàn)了,我們平時使用時是這樣的:Singleton.getInstance();
當方法被調(diào)用時Singleton第一次被使用庐扫,此時類被加載饭望。類加載過程中靜態(tài)變量被初始化仗哨,instance 實例也就是在這時候被構(gòu)建。
2.懶漢式
懶漢式通俗的解釋起來就是铅辞,懶人干的事情厌漂,懶人做事就是需要做的時候才去做。在代碼上的體現(xiàn)就是延時加載巷挥。
下面來一步步從 缺陷到 完善 實現(xiàn)懶漢式單例模式:
-
實現(xiàn) - 2 (存在缺陷的實現(xiàn))
我們經(jīng)常會寫以下代碼來實現(xiàn)單例模式桩卵,但是這種實現(xiàn)方式存在弊端:線程不安全。
示例代碼
public class Singleton {
private final static Singleton instance;
public static Singleton getInstance() {
if (instance== null) {
instance= new Singleton();
}
return instance;
}
}
我們來分析缺陷所在:
假設(shè)線程1倍宾、2同時調(diào)用getInstance()雏节,線程1準備執(zhí)行 instance= new Singleton(); 時被線程2預占。因為此時insteance 還未被示例話高职,所以線程2可以執(zhí)行完整個getInstance()方法钩乍,返回了Singleton對象引用。此時線程1在它停止的地方啟動怔锌,執(zhí)行接下來的代碼寥粹,由于已經(jīng)進行過了非空判斷,所以接下來就錯誤的再次實例化了一個Singleton對象埃元。此時就示例化了兩個Singleton對象涝涤。反之,如果能保證是單線程使用此單例對象岛杀,這種實現(xiàn)方式是沒有問題的阔拳。
-
實現(xiàn) - 3 (對實現(xiàn) - 2進行改進)
實現(xiàn)2中既然存在線程不安全的問題,那么很容易就想到一個處理方法类嗤,那就是加鎖糊肠。
示例代碼
public class Singleton {
private final static Singleton instance;
public static synchronized Singleton getInstance() {
if (instance== null) {
instance= new Singleton();
}
return instance;
}
}
這種實現(xiàn)與 實現(xiàn)2 相比較,差別就在于一個同步鎖遗锣。加了鎖的getInstance() 可以保證線程安全货裹,并且也實現(xiàn)了單例。
這一種正確的單例實現(xiàn)方式精偿,但是由于對 getInstance()做了同步處理弧圆,synchronized將導致性能開銷。
分析這種實現(xiàn)發(fā)現(xiàn)其實只有在第一次調(diào)用方法時才需要同步笔咽。(此處自行理解下)
由于只有第一次調(diào)用執(zhí)行了 instance= new Singleton()墓阀,而只有此行代碼需要同步,因此就無需對后續(xù)調(diào)用使用同步拓轻。除了第一次調(diào)用外其他的調(diào)用都只需要判斷 instance是否為 null斯撮,并將其返回。多線程能夠安全并發(fā)地執(zhí)行除第一次調(diào)用外的所有調(diào)用扶叉。
由于該方法是synchronized 的勿锅,需要為該方法的每一次調(diào)用付出同步的代價帕膜,即使只有第一次調(diào)用需要同步。所以如果getInstance()被多個線程頻繁的調(diào)用溢十,將會導致程序執(zhí)行性能的下降垮刹。反之,如果getInstance()不會被多個線程頻繁的調(diào)用张弛,那么這個延遲初始化方案將能提供令人滿意的性能荒典。
既然有性能上的不足,那么我們偉大的程序猿自然會想出優(yōu)化性能的方法吞鸭。所以就有了接下的的這種實現(xiàn) 雙重檢查鎖定
-
實現(xiàn) - 4 (對實現(xiàn) - 3進行性能優(yōu)化后的實現(xiàn) - 雙重檢查鎖定 double-checked locking)
先聲明寺董,雙重檢查鎖定這種實現(xiàn)方式是一種存在漏洞的單例實現(xiàn)
示例代碼
public class Singleton {
private final static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance== null) {
instance= new Singleton(); // 問題出現(xiàn)位置
}
}
}
return instance;
}
}
上面的代碼就是 雙重檢查鎖定的實現(xiàn)方式。
分析 實現(xiàn) - 4 的代碼:
如果第一次檢查instance不為null刻剥,那么就不需要執(zhí)行下面的加鎖和初始化操作遮咖。因此可以大幅降低synchronized帶來的性能開銷。上面代碼表面上看起來造虏,似乎兩全其美:
1. 在多個線程情況下同一時間調(diào)用getInstance()時御吞,會通過加鎖來保證只有一個線程能創(chuàng)建對象。
2.在對象創(chuàng)建好之后漓藕,執(zhí)行getInstance()將不需要每次都獲取鎖陶珠,直接返回已創(chuàng)建好的對象,優(yōu)化了實現(xiàn)-3 中多次獲取鎖導致的性能消耗享钞。
雙重檢查鎖定看起來似乎很完美揍诽,但這是一個錯誤的優(yōu)化!在線程執(zhí)行到第4行代碼讀取到instance不為null時嫩与,instance引用的對象有可能還沒有完成初始化。
該問題的具體分析請看這里 :http://www.infoq.com/cn/articles/double-checked-locking-with-delay-initialization
我在這里做簡單的分析并給出解決方式
前面的雙重檢查鎖定示例代碼的第7行instance = new Singleton()創(chuàng)建一個對象交排。這一行代碼可以分解為如下的三行偽代碼:
memory = allocate(); //1:分配對象的內(nèi)存空間
ctorInstance(memory); //2:初始化對象
instance = memory; //3:設(shè)置instance指向剛分配的內(nèi)存地址
上面三行偽代碼中的2和3之間划滋,可能會被重排序(在一些JIT編譯器上,這種重排序是真實發(fā)生的埃篓,詳情見參考文獻1的“Out-of-order writes”部分)处坪。2和3之間重排序之后的執(zhí)行時序如下:
memory = allocate(); //1:分配對象的內(nèi)存空間
instance = memory; //3:設(shè)置instance指向剛分配的內(nèi)存地址
//注意,此時對象還沒有被初始化架专!
ctorInstance(memory); //2:初始化對象
根據(jù)《The Java Language Specification, Java SE 7 Edition》(后文簡稱為java語言規(guī)范)同窘,所有線程在執(zhí)行java程序時必須要遵守intra-thread semantics。intra-thread semantics保證重排序不會改變單線程內(nèi)的程序執(zhí)行結(jié)果部脚。換句話來說想邦,intra-thread semantics允許那些在單線程內(nèi),不會改變單線程程序執(zhí)行結(jié)果的重排序委刘。上面三行偽代碼的2和3之間雖然被重排序了丧没,但這個重排序并不會違反intra-thread semantics鹰椒。這個重排序在沒有改變單線程程序的執(zhí)行結(jié)果的前提下,可以提高程序的執(zhí)行性能呕童。
為了更好的理解intra-thread semantics漆际,請看下面的示意圖(假設(shè)一個線程A在構(gòu)造對象后,立即訪問這個對象):
如上圖所示夺饲,只要保證2排在4的前面奸汇,即使2和3之間重排序了,也不會違反intra-thread semantics往声。
下面擂找,再讓我們看看多線程并發(fā)執(zhí)行的時候的情況。請看下面的示意圖:
上圖標識什么意思呢烁挟?
由于單線程內(nèi)要遵守intra-thread semantics婴洼,從而能保證A線程的程序執(zhí)行結(jié)果不會被改變。但是當線程A和B按上圖的時序執(zhí)行時撼嗓,B線程將看到一個還沒有被初始化的對象柬采。
這么說有點抽象,我們回到代碼分析
示例代碼第七行 instance= new Singleton() 且警,此處若是發(fā)生重排序粉捻,對象還未被初始化完成。此時另一個并發(fā)的線程B就有可能在 第4行判斷 instance 不為 null 斑芜。那么線程B就將訪問未完成初始化的對象肩刃。這就是錯誤所在。
在知曉了問題發(fā)生的根源之后杏头,我們可以想出兩個辦法來實現(xiàn)線程安全的延遲初始化:
1. 不允許2和3重排序盈包;
2. 允許2和3重排序,但不允許其他線程“看到”這個重排序醇王。
既然想到了方法呢燥,那么就用代碼來實現(xiàn)。
-
解決方案1:實現(xiàn) - 5 (基于volatile的雙重檢查鎖定)
示例代碼
public class Instance {
private volatile static Instance instance;
private Instance (){ }
public static Instance getInstance() {
if (instance== null) {
synchronized (SafeDoubleCheckedLocking.class) {
if (instance== null)
instance= new Instance();//instance 為volatile寓娩,現(xiàn)在沒問題了
}
}
return instance;
}
}
注意叛氨,這個解決方案需要JDK5或更高版本(因為從JDK5開始使用新的JSR-133內(nèi)存模型規(guī)范,這個規(guī)范增強了volatile的語義)棘伴。當聲明對象的引用為volatile后寞埠,“問題的根源”的三行偽代碼中的2和3之間的重排序,在多線程環(huán)境中將會被禁止焊夸。禁止后仁连,線程B在進行第一次 instance == null 判斷時就不會為true, 將按如下的時序執(zhí)行:
這個方案本質(zhì)上是通過禁止上圖中的2和3之間的重排序阱穗,來保證線程安全的延遲初始化怖糊。
解決方案2:實現(xiàn) - 6(基于類初始化的解決方案 - Initialization On Demand Holder idiom)
示例代碼
public class Singleton {
private Singleton() {
}
private static class LazyHolder {
static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return LazyHolder.INSTANCE; // 這里將導致Singleton類被初始化
}
}
JVM在類的初始化階段(即在Class被加載后帅容,且被線程使用之前),會執(zhí)行類的初始化伍伤。在執(zhí)行類的初始化期間并徘,JVM會去獲取一個鎖。這個鎖可以同步多個線程對同一個類的初始化扰魂。相比其他實現(xiàn)方案(如double-checked locking等)麦乞,該技術(shù)方案的實現(xiàn)代碼較為簡潔,并且在所有版本的編譯器中都是可行的劝评。
補充內(nèi)容
關(guān)于實現(xiàn) - 6中姐直,static final Instance instance 域的訪問權(quán)限為什么是包級私有可以讀:Initialization On Demand Holder idiom的實現(xiàn)探討
各種實現(xiàn)方式的優(yōu)缺點:
-
餓漢式(實現(xiàn) - 1) 單例實例在類裝載時就構(gòu)建,急切初始化蒋畜。
- 優(yōu)點:
- 線程安全
- 在類加載的同時已經(jīng)創(chuàng)建好一個靜態(tài)對象声畏,調(diào)用時反應(yīng)速度快。
- 缺點
- 資源效率不高姻成,getInstance()可能永遠不會執(zhí)行到插龄,但執(zhí)行該類的其他靜態(tài)方法或者加載了該類(class.forName),那么這個實例仍然初始化科展。
- 優(yōu)點:
-
懶漢式 (實現(xiàn) 2 - 6)單例實例在第一次被使用時構(gòu)建(調(diào)用 getInsteance())均牢,延遲初始化。
- 實現(xiàn) - 2(缺陷實現(xiàn)):這種實現(xiàn)方式存在線程不安全的缺陷才睹,不推薦使用徘跪。但若能保證處于單線程中,可以使用這種實現(xiàn)方式琅攘。
- 實現(xiàn) - 3(耗資源實現(xiàn)):這種實現(xiàn)方式對實現(xiàn)-2中線程不安全的缺陷進行了處理垮庐。
- 優(yōu)點:資源利用率高,不執(zhí)行g(shù)etInstance()就不會被實例坞琴,可以執(zhí)行該類的其他靜態(tài)方法哨查。
- 缺點:第一次加載時不夠快,多線程使用不必要的同步開銷大
- 實現(xiàn) - 4(問題實現(xiàn)):雙重檢查鎖定置济,這是對實現(xiàn) - 3的一種優(yōu)化實現(xiàn)解恰,但是存在重排序?qū)е芦@取到未初始化的單例對象的問題
- 實現(xiàn) - 5:雙重檢查鎖定+volatile
- 優(yōu)點:資源利用率高锋八,不執(zhí)行g(shù)etInstance()就不會被實例浙于,可以執(zhí)行該類的其他靜態(tài)方法。
- 缺點:第一次加載時反應(yīng)不快挟纱。實現(xiàn)起來代碼較為復雜羞酗。
- 注意點:jdk1.5版本后volatile關(guān)鍵字才能正確的工作。Android平臺不同當心這個問題紊服,一般Android都是jdk1.6以上檀轨。
- 實現(xiàn) - 6:靜態(tài)內(nèi)部類
- 優(yōu)點:資源利用率高胸竞,不執(zhí)行g(shù)etInstance()就不會被實例,可以執(zhí)行該類的其他靜態(tài)方法参萄。實現(xiàn)代碼較為簡潔
- 缺點:第一次加載時反應(yīng)不快卫枝。
總結(jié):
- 延遲初始化降低了初始化類或創(chuàng)建實例的開銷,但增加了訪問被延遲初始化的字段的開銷(鎖)讹挎。在大多數(shù)時候校赤,正常的初始化要優(yōu)于延遲初始化。
- 如果確實需要對實例字段使用線程安全的延遲初始化筒溃,請使用上面介紹的基于volatile的延遲初始化的方案(實現(xiàn) - 5)马篮;
- 如果確實需要對靜態(tài)字段使用線程安全的延遲初始化,請使用上面介紹的基于類初始化的方案怜奖。(實現(xiàn) - 6)
- 一般采用餓漢式(實現(xiàn) - 1)浑测,若對資源十分在意建議采用靜態(tài)內(nèi)部類(實現(xiàn) - 6)。