老司機來教你單例的正確姿勢

老司機來教你單例的正確姿勢

Java單例模式可能是最簡單也是最常用的設(shè)計模式,一個完美的單例需要做到哪些事呢?

  1. 單例(這不是廢話嗎)
  2. 延遲加載
  3. 線程安全
  4. 沒有性能問題
  5. 防止序列化產(chǎn)生新對象
  6. 防止反射攻擊

可以看到,真正要實現(xiàn)一個完美的單例是很復(fù)雜的,那么,讓我這個司機帶大家看一看正確姿勢的單例。

最佳實踐單例之枚舉

沒錯,直接就上最佳實踐,就是這么任性

這貨長這樣:

public enum Singleton{
    INSTANCE;
}

如果你不熟悉枚舉,可能會說:這貨是啥?!

這種方式的好處是:

  1. 利用的枚舉的特性實現(xiàn)單例
  2. 由JVM保證線程安全
  3. 序列化和反射攻擊已經(jīng)被枚舉解決

調(diào)用方式為Singleton.INSTANCE, 出自《Effective Java》第二版第三條: 用私有構(gòu)造器或枚舉類型強化Singleton屬性。

關(guān)于單例最佳實踐的討論可以看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)容中就不再復(fù)述了。

最體現(xiàn)技術(shù)的單例之懶漢式

懶漢式即實現(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關(guān)鍵字就行了

public static synchronized Singleton getInstance() {
    if (INSTANCE == null) {
        INSTANCE = new Singleton();
    }
    return INSTANCE;
}

可是...這樣又出現(xiàn)了性能問題,簡單粗暴的同步整個方法,導(dǎo)致同一時間內(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 件事情。

  1. 給 instance 分配內(nèi)存
  2. 調(diào)用 Singleton 的構(gòu)造函數(shù)來初始化成員變量
  3. 將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 中才得以修復(fù),所以在這之后才可以放心使用 volatile。

至此,這樣的懶漢式才是沒有問題的懶漢式。

內(nèi)部類實現(xiàn)單例

public class Singleton { 
    /** 
     * 類級的內(nèi)部類,也就是靜態(tài)的成員式內(nèi)部類,該內(nèi)部類的實例與外部類的實例沒有綁定關(guān)系, 
     * 而且只有被調(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() 方法也無需使用同步關(guān)鍵字。

總結(jié)

無疑,單例就應(yīng)使用枚舉實現(xiàn),最佳實踐誠不欺我

參考鏈接

What is an efficient way to implement a singleton pattern in Java

Java Practices -> Singleton

Creating and Destroying Java Objects: Part 1

如何正確地寫出單例模式

JAVA 枚舉單例模式

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容