老司機來教你單例的正確姿勢
Java單例模式可能是最簡單也是最常用的設計模式,一個完美的單例需要做到哪些事呢娜氏?
- 單例(這不是廢話嗎)
- 延遲加載
- 線程安全
- 沒有性能問題
- 防止序列化產(chǎn)生新對象
- 防止反射攻擊
可以看到,真正要實現(xiàn)一個完美的單例是很復雜的,那么详炬,讓我這個司機帶大家看一看正確姿勢的單例梨水。
最佳實踐單例之枚舉
沒錯赋铝,直接就上最佳實踐泼菌,就是這么任性
這貨長這樣:
public enum Singleton{
INSTANCE;
}
如果你不熟悉枚舉,可能會說:這貨是啥琅关?颂砸!
這種方式的好處是:
- 利用的枚舉的特性實現(xiàn)單例
- 由JVM保證線程安全
- 序列化和反射攻擊已經(jīng)被枚舉解決
調(diào)用方式為Singleton.INSTANCE
, 出自《Effective Java》第二版第三條: 用私有構(gòu)造器或枚舉類型強化Singleton屬性。
關于單例最佳實踐的討論可以看Stackoverflow:what-is-an-efficient-way-to-implement-a-singleton-pattern-in-java
下面將會介紹更為常見的單例模式,但是均未處理反射攻擊人乓,如果想了解更多可以看這篇文章:如何防止單例模式被JAVA反射攻擊
最簡單的單例之餓漢式
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
// 私有化構(gòu)造函數(shù)
private Singleton(){}
public static Singleton getInstance(){
return INSTANCE;
}
}
這種單例的寫法最簡單勤篮,但是缺點是一旦類被加載,單例就會初始化色罚,沒有實現(xiàn)懶加載碰缔。而且當實現(xiàn)了Serializable接口后,反序列化時單例會被破壞戳护。
實現(xiàn)Serializable接口需要重寫readResolve
金抡,才能保證其反序列化依舊是單例:
public class Singleton implements Serializable {
private static final Singleton INSTANCE = new Singleton();
// 私有化構(gòu)造函數(shù)
private Singleton(){}
public static Singleton getInstance(){
return INSTANCE;
}
/**
* 如果實現(xiàn)了Serializable, 必須重寫這個方法
*/
private Object readResolve() throws ObjectStreamException {
return INSTANCE;
}
}
OK,反序列化要注意的就是這一點腌且,下面的內(nèi)容中就不再復述了梗肝。
最體現(xiàn)技術的單例之懶漢式
懶漢式即實現(xiàn)延遲加載的單例,為上述餓漢式的優(yōu)化形式铺董。而因其仍需要進一步優(yōu)化巫击,往往成為面試考點,讓我們一起來看看坑爹的“懶漢式”
懶漢式的最初形式是這樣的:
public class Singleton {
private static Singleton INSTANCE;
private Singleton (){}
public static Singleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
這種寫法就輕松實現(xiàn)了單例的懶加載精续,只有調(diào)用了getInstance
方法才會初始化坝锰。但是這樣的寫法出現(xiàn)了新的問題--線程不安全。當多個線程調(diào)用getInstance
方法時重付,可能會創(chuàng)建多個實例顷级,因此需要對其進行同步。
如何使其線程安全呢确垫?簡單弓颈,加個synchronized
關鍵字就行了
public static synchronized Singleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
可是...這樣又出現(xiàn)了性能問題,簡單粗暴的同步整個方法删掀,導致同一時間內(nèi)只有一個線程能夠調(diào)用getInstance
方法恨豁。
因為僅僅需要對初始化部分的代碼進行同步,所以再次進行優(yōu)化:
public static Singleton getSingleton() {
if (INSTANCE == null) { // 第一次檢查
synchronized (Singleton.class) {
if (INSTANCE == null) { // 第二次檢查
INSTANCE = new Singleton();
}
}
}
return INSTANCE ;
}
執(zhí)行兩次檢測很有必要:當多線程調(diào)用時爬迟,如果多個線程同時執(zhí)行完了第一次檢查,其中一個進入同步代碼塊創(chuàng)建了實例菊匿,后面的線程因第二次檢測不會創(chuàng)建新實例付呕。
這段代碼看起來很完美,但仍舊存在問題跌捆,以下內(nèi)容引用自黑桃夾克大神的如何正確地寫出單例模式
這段代碼看起來很完美徽职,很可惜,它是有問題佩厚。主要在于instance = new Singleton()這句姆钉,這并非是一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情。
- 給 instance 分配內(nèi)存
- 調(diào)用 Singleton 的構(gòu)造函數(shù)來初始化成員變量
- 將instance對象指向分配的內(nèi)存空間(執(zhí)行完這步 instance 就為非 null 了)
但是在 JVM 的即時編譯器中存在指令重排序的優(yōu)化潮瓶。也就是說上面的第二步和第三步的順序是不能保證的陶冷,最終的執(zhí)行順序可能是 1-2-3 也可能是 1-3-2。如果是后者毯辅,則在 3 執(zhí)行完畢埂伦、2 未執(zhí)行之前,被線程二搶占了思恐,這時 instance 已經(jīng)是非 null 了(但卻沒有初始化)沾谜,所以線程二會直接返回 instance,然后使用胀莹,然后順理成章地報錯基跑。
我們只需要將 instance 變量聲明成 volatile 就可以了。
public class Singleton {
private volatile static Singleton INSTANCE; //聲明成 volatile
private Singleton (){}
public static Singleton getSingleton() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
使用 volatile 的主要原因是其另一個特性:禁止指令重排序優(yōu)化描焰。也就是說媳否,在 volatile 變量的賦值操作后面會有一個內(nèi)存屏障(生成的匯編代碼上),讀操作不會被重排序到內(nèi)存屏障之前栈顷。比如上面的例子逆日,取操作必須在執(zhí)行完 1-2-3 之后或者 1-3-2 之后,不存在執(zhí)行到 1-3 然后取到值的情況萄凤。從「先行發(fā)生原則」的角度理解的話室抽,就是對于一個 volatile 變量的寫操作都先行發(fā)生于后面對這個變量的讀操作(這里的“后面”是時間上的先后順序)。
但是特別注意在 Java 5 以前的版本使用了 volatile 的雙檢鎖還是有問題的靡努。其原因是 Java 5 以前的 JMM (Java 內(nèi)存模型)是存在缺陷的坪圾,即時將變量聲明成 volatile 也不能完全避免重排序,主要是 volatile 變量前后的代碼仍然存在重排序問題惑朦。這個 volatile 屏蔽重排序的問題在 Java 5 中才得以修復兽泄,所以在這之后才可以放心使用 volatile。
至此漾月,這樣的懶漢式才是沒有問題的懶漢式病梢。
內(nèi)部類實現(xiàn)單例
public class Singleton {
/**
* 類級的內(nèi)部類,也就是靜態(tài)的成員式內(nèi)部類梁肿,該內(nèi)部類的實例與外部類的實例沒有綁定關系蜓陌,
* 而且只有被調(diào)用到才會裝載,從而實現(xiàn)了延遲加載
*/
private static class SingletonHolder{
/**
* 靜態(tài)初始化器吩蔑,由JVM來保證線程安全
*/
private static final Singleton instance = new Singleton();
}
/**
* 私有化構(gòu)造方法
*/
private Singleton(){
}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
}
使用內(nèi)部類來維護單例的實例钮热,當Singleton被加載時,其內(nèi)部類并不會被初始化烛芬,故可以確保當 Singleton類被載入JVM時隧期,不會初始化單例類飒责。只有 getInstance()
方法調(diào)用時,才會初始化 instance仆潮。同時宏蛉,由于實例的建立是時在類加載時完成,故天生對多線程友好鸵闪,getInstance()
方法也無需使用同步關鍵字檐晕。
總結(jié)
無疑,單例就應使用枚舉實現(xiàn)蚌讼,最佳實踐誠不欺我
參考鏈接
What is an efficient way to implement a singleton pattern in Java