在項(xiàng)目開發(fā)時(shí)有一些對(duì)象其實(shí)我們只需要一個(gè),比如:線程池娩井、緩存暇屋、日志對(duì)象等等。這類對(duì)象只能有一個(gè)實(shí)例洞辣,如果制造出多個(gè)實(shí)例咐刨,就會(huì)導(dǎo)致許多問題產(chǎn)生,例如:程序的行為異常扬霜,資源使用過量定鸟,或者是不一致的結(jié)果。
雖然程序員之間的約定以及全局變量也可以辦得到著瓶,但是單例模式確實(shí)是經(jīng)得起時(shí)間考驗(yàn)的更好的做法联予。單例模式和全局變量一樣方便的給我們提供了一個(gè)全局的訪問點(diǎn),但是也解決了全局變量必須在程序一開始就要?jiǎng)?chuàng)建好對(duì)象的缺點(diǎn)材原。單例模式可以靈活的決定對(duì)象什么時(shí)候創(chuàng)建沸久。
結(jié)構(gòu)定義
單例模式: 保證一個(gè)類僅有一個(gè)實(shí)例,并且提供一個(gè)訪問它的全局訪問點(diǎn)余蟹。
通常我們可以讓一個(gè)全局變量使得一個(gè)對(duì)象被訪問卷胯,但它不能防止你實(shí)例化多個(gè)對(duì)象。一個(gè)最好的辦法就是讓類自身負(fù)責(zé)保存它的唯一實(shí)例威酒。這個(gè)類可以保證沒有其它實(shí)例可以被創(chuàng)建,并且它可以提供一個(gè)訪問該實(shí)例的方法窑睁。[DP]
單例模式的寫法(7種)
單例模式的思路
- 利用一個(gè)靜態(tài)變量
INSTANCE
來記錄類的唯一實(shí)例 - 把構(gòu)造器聲明為私有的挺峡,只有在類本身才能調(diào)用構(gòu)造器
- 用
getInstance()
方法實(shí)例化對(duì)象,并返回這個(gè)類的實(shí)例
分析:
利用靜態(tài)變量來保存類的實(shí)例確保該實(shí)例為類的唯一實(shí)例卵慰,如果實(shí)例為空沙郭,則表示還沒有創(chuàng)建實(shí)例,而如果不存在我們就利用私有的構(gòu)造器產(chǎn)生一個(gè)該類實(shí)例并把它賦值到靜態(tài)變量中裳朋,如果我們不需要這個(gè)實(shí)例病线,它就永遠(yuǎn)不會(huì)產(chǎn)生。這個(gè)就是延遲實(shí)例化
- 懶漢模式(線程不安全)
public class Singleton {
private static Singleton INSTANCE;
private Singleton() {
}
public static Singleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
這段代碼簡(jiǎn)單明了鲤嫡,而且使用了懶加載模式送挑,但是卻存在致命的問題。當(dāng)有多個(gè)線程并行調(diào)用 getInstance()
的時(shí)候暖眼,就會(huì)創(chuàng)建多個(gè)實(shí)例惕耕。也就是說在多線程下不能正常工作。
-
懶漢模式(線程安全)
解決懶漢模式線程安全問題诫肠,最簡(jiǎn)單的方法是將整個(gè)getInstance()
方法設(shè)為同步synchronized
司澎。
public class Singleton {
private static Singleton INSTANCE;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
雖然做到了線程安全,并且解決了多實(shí)例的問題栋豫,但是它并不高效挤安。因?yàn)樵谌魏螘r(shí)候只能有一個(gè)線程調(diào)用 getInstance()
方法。但是同步操作只需要在第一次調(diào)用時(shí)才被需要丧鸯,即第一次創(chuàng)建單例實(shí)例對(duì)象時(shí)蛤铜。這就引出了雙重檢驗(yàn)鎖。
-
雙重校驗(yàn)鎖 *
雙重檢驗(yàn)鎖模式(double checked locking pattern)丛肢,是一種使用同步塊加鎖的方法围肥。程序員稱其為雙重檢查鎖,因?yàn)闀?huì)有兩次檢查instance == null
蜂怎,一次是在同步塊外穆刻,一次是在同步塊內(nèi)。為什么在同步塊內(nèi)還要再檢驗(yàn)一次派敷?因?yàn)榭赡軙?huì)有多個(gè)線程一起進(jìn)入同步塊外的 if蛹批,如果在同步塊內(nèi)不進(jìn)行二次檢驗(yàn)的話就會(huì)生成多個(gè)實(shí)例了。
public static Singleton getInstance() {
if (INSTANCE == null) { // 一重
synchronized (Singleton.class) {
if (INSTANCE == null) { // 二重
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
這段代碼會(huì)有一個(gè)隱藏問題篮愉,主要是在INSTANCE = new Singleton()
這句涉及到了JVM編譯器的指令重排,這并非是一個(gè)原子操作差导,事實(shí)上在 JVM 中這句話大概做了下面 3 件事情:
- 給 instance 分配內(nèi)存
- 調(diào)用 Singleton 的構(gòu)造函數(shù)來初始化成員變量
- 將instance對(duì)象指向分配的內(nèi)存空間(執(zhí)行完這步 instance 就為非 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ò)掀泳。
我們只需要將 instance 變量聲明成 volatile 就可以了。
public class Singleton {
private static volatile Singleton INSTANCE;
private Singleton() {
}
/**
* 雙重校驗(yàn)鎖
*/
public static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
關(guān)于volatile
修飾符用最簡(jiǎn)單的方式理解就是阻止了變量訪問前后的指令重排西轩,保證了指令執(zhí)行順序员舵。
- 餓漢模式
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
這種方法非常簡(jiǎn)單,因?yàn)閱卫膶?shí)例被聲明成 static
和 final
變量了藕畔,在第一次加載類到內(nèi)存中時(shí)就會(huì)初始化马僻,所以創(chuàng)建實(shí)例本身是線程安全的。
這種方式基于classloder
機(jī)制避免了多線程的同步問題注服,不過韭邓,instance
在類裝載時(shí)就實(shí)例化,雖然導(dǎo)致類裝載的原因有很多種溶弟,在單例模式中大多數(shù)都是調(diào)用getInstance
方法女淑, 但是也不能確定有其他的方式(或者其他的靜態(tài)方法)導(dǎo)致類裝載,這時(shí)候初始化instance
顯然沒有達(dá)到lazy loading
的效果可很。
- 餓漢模式(變種)
private static Singleton instance;
static {
instance = new Singleton();
}
public static Singleton getInstance() {
return instance;
}
這種寫法本質(zhì)上和上一種寫法沒什么區(qū)別诗力。
- 靜態(tài)內(nèi)部類
private Singleton() {
}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
注意:
- 從外部無法訪問靜態(tài)內(nèi)部類SingletonHolder,只有當(dāng)調(diào)用Singleton.getInstance方法的時(shí)候我抠,才能得到單例對(duì)象INSTANCE苇本。
- INSTANCE對(duì)象初始化的時(shí)機(jī)并不是在單例類Singleton被加載的時(shí)候,而是在調(diào)用getInstance方法菜拓,使得靜態(tài)內(nèi)部類SingletonHolder被加載的時(shí)候瓣窄。因此這種實(shí)現(xiàn)方式是利用classloader的加載機(jī)制來實(shí)現(xiàn)懶加載,并保證構(gòu)建單例的線程安全纳鼎。
- 無法防止利用反射重復(fù)構(gòu)建對(duì)象
-
枚舉高效寫法
在《Effective Java》最后推薦了這樣一個(gè)寫法俺夕,簡(jiǎn)直有點(diǎn)顛覆,不僅超級(jí)簡(jiǎn)單贱鄙,而且保證了線程安全劝贸。這里引用一下,此方法無償提供了序列化機(jī)制逗宁,絕對(duì)防止多次實(shí)例化映九,及時(shí)面對(duì)復(fù)雜的序列化或者反射攻擊。單元素枚舉類型已經(jīng)成為實(shí)現(xiàn)Singleton的最佳方法瞎颗。
public enum Singleton {
/**
*
*/
INSTANCE;
/**
*
*/
public void hello() {
System.out.println("Hello World");
}
}
對(duì)于一個(gè)標(biāo)準(zhǔn)的enum單例模式件甥,最優(yōu)秀的寫法還是實(shí)現(xiàn)接口的形式:
public enum Singleton implements MySingleton {
/**
*
*/
INSTANCE {
@Override
public void hello() {
System.out.println("Hello world");
}
}
}
interface MySingleton {
/**
* xx
*/
void hello();
}
使用枚舉實(shí)現(xiàn)單例模式不僅防止了反射構(gòu)建對(duì)象也保證了線程安全捌议,但是同時(shí)它并不是懶加載,在枚舉類加載的同時(shí)引有,其單例對(duì)象就已經(jīng)被初始化瓣颅。
總結(jié)
單例模式寫法總結(jié)起來可以分為五種懶漢
、惡漢
譬正、雙重校驗(yàn)鎖
宫补、枚舉
、靜態(tài)內(nèi)部類
导帝,上述所說都是線程安全的實(shí)現(xiàn)守谓,第一種應(yīng)該說是不正確的實(shí)現(xiàn)。
對(duì)于這幾種的比較
單例模式 | 是否線程安全 | 是否懶加載 | 是否防止反射構(gòu)建 |
---|---|---|---|
雙重校驗(yàn)鎖 | 是 | 是 | 否 |
枚舉 | 是 | 否 | 是 |
靜態(tài)內(nèi)部類 | 是 | 是 | 否 |
補(bǔ)充
- volatile關(guān)鍵字不但可以防止指令重排您单,也可以保證線程訪問的變量值是主內(nèi)存中的最新值斋荞。有關(guān)volatile的詳細(xì)原理,我在以后的漫畫中會(huì)專門講解虐秦。
- 使用枚舉實(shí)現(xiàn)的單例模式平酿,不但可以防止利用反射強(qiáng)行構(gòu)建單例對(duì)象,而且可以在枚舉類對(duì)象被反序列化的時(shí)候悦陋,保證反序列的返回結(jié)果是同一對(duì)象蜈彼。
對(duì)于其他方式實(shí)現(xiàn)的單例模式,如果既想要做到可序列化俺驶,又想要反序列化為同一對(duì)象幸逆,則必須實(shí)現(xiàn)readResolve方法。- 應(yīng)該在任何情況下都應(yīng)實(shí)現(xiàn)線程安全的寫法暮现。