單例模式
-
轉(zhuǎn)載原文鏈接-張新強(qiáng)
[TOC]
單例對(duì)象的類必須保證只有一個(gè)實(shí)例存在
- 懶漢式:指全局的單例實(shí)例在第一次被使用時(shí)構(gòu)建
- 餓漢式:指全局單例實(shí)例在類裝載時(shí)構(gòu)建
1. 懶漢式單例
1.1 最簡(jiǎn)單版本
//version1
public class Single1{
private static Single1 instance;
public static Single1 getInstance(){
if (instance == null)
instance = new Single1();
return instance;
}
}
或者進(jìn)一步,把構(gòu)造器改為私有,這樣能夠防止被外部的類調(diào)用。
// Version 1.1
public class Single1 {
private static Single1 instance;
private Single1() {}
public static Single1 getInstance() {
if (instance == null) {
instance = new Single1();
}
return instance;
}
}
這種寫法在大多數(shù)的時(shí)候也是沒問題的。
問題在于,當(dāng)多線程工作的時(shí)候,如果有多個(gè)線程同時(shí)運(yùn)行到if (instance == null),都判斷為null,那么兩個(gè)線程就各自會(huì)創(chuàng)建一個(gè)實(shí)例——這樣一來,就不是單例了
1.2 synchronized版本 ['s??kr?na?zd]
// Version 2
public class Single2 {
private static Single2 instance;
private Single2() {}
public static synchronized Single2 getInstance() {
if (instance == null) {
instance = new Single2();
}
return instance;
}
}
但是,這種寫法也有一個(gè)問題:給gitInstance方法加鎖,雖然避免了可能會(huì)出現(xiàn)的多個(gè)實(shí)例問題,但是會(huì)被強(qiáng)制除T1之外的所有線程等待,實(shí)際上會(huì)對(duì)程序的執(zhí)行效率造成負(fù)面影響。
1.3 雙重檢查的版本(Double-Check)版本
Version2代碼相對(duì)于Version1d代碼的效率問題,其實(shí)是為了解決1%幾率的問題,而使用了一個(gè)100%出現(xiàn)的防護(hù)盾。那有一個(gè)優(yōu)化的思路,就是把100%出現(xiàn)的防護(hù)盾,也改為1%的幾率出現(xiàn),使之只出現(xiàn)在可能會(huì)導(dǎo)致多個(gè)實(shí)例出現(xiàn)的地方。
——有沒有這樣的方法呢?當(dāng)然是有的,改進(jìn)后的代碼Vsersion3如下:
//version3
public class Single3{
private static Single3 instance;
private Single3(){}
public static Single3 getInstance(){
if (instance == null){
synchronized (Single3.class){
if (instance == null)
instance = new Single3();
}
}
return instance;
}
}
這個(gè)版本的代碼看起來有點(diǎn)復(fù)雜,注意其中有兩次if (instance == null)的判斷,這個(gè)叫做『雙重檢查 Double-Check』
- 第一個(gè)if (instance == null),其實(shí)是為了解決Version2中的效率問題,只有instance為null的時(shí)候,才進(jìn)入synchronized的代碼段——大大減少了幾率。
- 第二個(gè)if (instance == null),則是跟Version2一樣,是為了防止可能出現(xiàn)多個(gè)實(shí)例的情況
—— 這段代碼看起來已經(jīng)完美無瑕了。
……
……
……
—— 當(dāng)然,只是『看起來』,還是有小概率出現(xiàn)問題的。
這弄清楚為什么這里可能出現(xiàn)問題,首先,我們需要弄清楚幾個(gè)概念:原子操作、指令重排。
知識(shí)點(diǎn):什么是指令重排?
簡(jiǎn)單來說,就是計(jì)算機(jī)為了提高執(zhí)行效率,會(huì)做的一些優(yōu)化,在不影響最終結(jié)果的情況下,可能會(huì)對(duì)一些語(yǔ)句的執(zhí)行順序進(jìn)行調(diào)整
——也就是說,對(duì)于非原子性的操作,在不影響最終結(jié)果的情況下,其拆分成的原子操作可能會(huì)被重新排列執(zhí)行順序。
下面這段話直接從陳皓的文章(深入淺出單實(shí)例SINGLETON設(shè)計(jì)模式)中復(fù)制而來:
主要在于singleton = new Singleton()這句,這并非是一個(gè)原子操作,事實(shí)上在 JVM 中這句話大概做了下面 3 件事情。
1. 給 singleton 分配內(nèi)存
2. 調(diào)用 Singleton 的構(gòu)造函數(shù)來初始化成員變量,形成實(shí)例
3. 將singleton對(duì)象指向分配的內(nèi)存空間(執(zhí)行完這步 singleton才是非 null 了)
但是在 JVM 的即時(shí)編譯器中存在指令重排序的優(yōu)化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執(zhí)行順序可能是 1-2-3 也可能是 1-3-2。如果是后者,則在 3 執(zhí)行完畢、2 未執(zhí)行之前,被線程二搶占了,這時(shí) instance 已經(jīng)是非 null 了(但卻沒有初始化),所以線程二會(huì)直接返回 instance,然后使用,然后順理成章地報(bào)錯(cuò)。
1.4 終極版本:volatile
對(duì)于Version3中可能出現(xiàn)的問題(當(dāng)然這種概率已經(jīng)非常小了,但畢竟還是有的嘛~),解決方案是:只需要給instance的聲明加上volatile關(guān)鍵字即可,Version4版本:
//Version4
public class Single4{
private static volatile Single4 instance;
private Single4(){}
public static Single4 getInstance(){
if (instance == null){
synchronized (Single4.class){
if (instance == null){
instance = new Single4();
}
}
}
return instance;
}
}
-
volatile關(guān)鍵字的一個(gè)作用就是禁止指令重排,把instance聲明為volatile之后,對(duì)它的寫操作就會(huì)有一個(gè)內(nèi)存屏蔽,這樣在它賦值之前,就不用調(diào)用讀操作。
注意:volatile阻止的不singleton = new Singleton()這句話內(nèi)部[1-2-3]的指令重排,而是保證了在一個(gè)寫操作([1-2-3])完成之前,不會(huì)調(diào)用讀操作(if (instance == null))。
2. 餓漢式單例
如上所說,餓漢式單例是指:指全局的單例實(shí)例在類裝載時(shí)構(gòu)建的實(shí)現(xiàn)方式。
2.1 餓漢式單例的實(shí)現(xiàn)方式
//餓漢式實(shí)現(xiàn)
public class SingleB {
private static final SingleB INSTANCE = new SingleB();
private SingleB() {}
public static SingleB getInstance() {
return INSTANCE;
}
}
對(duì)于一個(gè)餓漢式單例的寫法來說,它基本上是完美的了。
所以它的缺點(diǎn)也就只是餓漢式單例本身的缺點(diǎn)所在了——由于INSTANCE的初始化是在類加載時(shí)進(jìn)行的,而類的加載是由ClassLoader來做的,所以開發(fā)者本來對(duì)于它初始化的時(shí)機(jī)就很難去準(zhǔn)確把握:
- 可能由于初始化的太早,造成資源的浪費(fèi)
- 如果初始化本身依賴于一些其他數(shù)據(jù),那么也就很難保證其他數(shù)據(jù)會(huì)在它初始化之前準(zhǔn)備好。
當(dāng)然,如果所需的單例占用的資源很少,并且也不依賴于其他數(shù)據(jù),那么這種實(shí)現(xiàn)方式也是很好的
2.2 其他的實(shí)現(xiàn)方式
2.2.1 Effective Java 1 —— 靜態(tài)內(nèi)部類
// Effective Java 第一版推薦寫法
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
這種寫法非常巧妙:
- 對(duì)于內(nèi)部類SingletonHolder,它是一個(gè)餓漢式的單例實(shí)現(xiàn),在SingletonHolder初始化的時(shí)候會(huì)由ClassLoader來保證同步,使INSTANCE是一個(gè)真·單例。
- 同時(shí),由于SingletonHolder是一個(gè)內(nèi)部類,只在外部類的Singleton的getInstance()中被使用,所以它被加載的時(shí)機(jī)也就是在getInstance()方法第一次被調(diào)用的時(shí)候。
——它利用了ClassLoader來保證了同步,同時(shí)又能讓開發(fā)者控制類加載的時(shí)機(jī)。從內(nèi)部看是一個(gè)餓漢式的單例,但是從外部看來,又的確是懶漢式的實(shí)現(xiàn)。
2.2.2 Effective Java 2 —— 枚舉
// Effective Java 第二版推薦寫法
public enum SingleInstance {
INSTANCE;
public void fun1() {
// do something
}
}
// 使用
SingleInstance.INSTANCE.fun1();
看到了么?這是一個(gè)枚舉類型……連class都不用了,極簡(jiǎn)。
由于創(chuàng)建枚舉實(shí)例的過程是線程安全的,所以這種寫法也沒有同步的問題。
作者對(duì)這方方法的評(píng)價(jià):
這種寫法在功能上與共有域方法相近,但是它更簡(jiǎn)潔,無償?shù)靥峁┝诵蛄谢瘷C(jī)制,絕對(duì)防止對(duì)此實(shí)例化,即使是在面對(duì)復(fù)雜的序列化或者反射攻擊的時(shí)候。雖然這中方法還沒有廣泛采用,但是單元素的枚舉類型已經(jīng)成為實(shí)現(xiàn)Singleton的最佳方法。
3. 總結(jié)
OK,看到這里,你還會(huì)覺得單例模式是最簡(jiǎn)單的設(shè)計(jì)模式了么?再回頭看一下你之前代碼中的單例實(shí)現(xiàn),覺得是無懈可擊的么?
可能我們?cè)趯?shí)際的開發(fā)中,對(duì)單例的實(shí)現(xiàn)并沒有那么嚴(yán)格的要求。比如,我如果能保證所有的getInstance都是在一個(gè)線程的話,那其實(shí)第一種最簡(jiǎn)單的教科書方式就夠用了。再比如,有時(shí)候,我的單例變成了多例也可能對(duì)程序沒什么太大影響……
但是,如果我們能了解更多其中的細(xì)節(jié),那么如果哪天程序出了些問題,我們起碼能多一個(gè)排查問題的點(diǎn)。早點(diǎn)解決問題,就能早點(diǎn)回家吃飯……:-D
—— 還有,完美的方案是不存在,任何方式都會(huì)有一個(gè)『度』的問題。比如,你的覺得代碼已經(jīng)無懈可擊了,但是因?yàn)槟阌玫氖荍AVA語(yǔ)言,可能ClassLoader有些BUG啊……你的代碼誰(shuí)運(yùn)行在JVM上的,可能JVM本身有BUG啊……你的代碼運(yùn)行在手機(jī)上,可能手機(jī)系統(tǒng)有問題啊……你生活在這個(gè)宇宙里,可能宇宙本身有些BUG啊……o(╯□╰)o
所以,盡力做到能做到的最好就行了。
—— 感謝你花費(fèi)了不少時(shí)間看到這里,但愿你沒有覺得虛度。
4. 一些有用的鏈接
- 深入淺出單實(shí)例SINGLETON設(shè)計(jì)模式:http://coolshell.cn/articles/265.html
- Java并發(fā)編程:volatile關(guān)鍵字解析:http://www.cnblogs.com/dolphin0520/p/3920373.html
- 為什么volatile不能保證原子性而Atomic可以?: http://www.cnblogs.com/Mainz/p/3556430.html
- 類在什么時(shí)候加載和初始化?http://www.importnew.com/6579.html