設計模式 - 單例模式

在項目開發(fā)時有一些對象其實我們只需要一個,比如:線程池、緩存、日志對象等等。這類對象只能有一個實例,如果制造出多個實例,就會導致許多問題產(chǎn)生,例如:程序的行為異常,資源使用過量,或者是不一致的結(jié)果。

雖然程序員之間的約定以及全局變量也可以辦得到,但是單例模式確實是經(jīng)得起時間考驗的更好的做法。單例模式和全局變量一樣方便的給我們提供了一個全局的訪問點,但是也解決了全局變量必須在程序一開始就要創(chuàng)建好對象的缺點。單例模式可以靈活的決定對象什么時候創(chuàng)建。

結(jié)構定義

單例模式

單例模式: 保證一個類僅有一個實例,并且提供一個訪問它的全局訪問點。

通常我們可以讓一個全局變量使得一個對象被訪問,但它不能防止你實例化多個對象。一個最好的辦法就是讓類自身負責保存它的唯一實例。這個類可以保證沒有其它實例可以被創(chuàng)建,并且它可以提供一個訪問該實例的方法。[DP]

單例模式的寫法(7種)

單例模式的思路

  1. 利用一個靜態(tài)變量INSTANCE來記錄類的唯一實例
  2. 把構造器聲明為私有的,只有在類本身才能調(diào)用構造器
  3. getInstance()方法實例化對象,并返回這個類的實例

分析:
利用靜態(tài)變量來保存類的實例確保該實例為類的唯一實例,如果實例為空,則表示還沒有創(chuàng)建實例,而如果不存在我們就利用私有的構造器產(chǎn)生一個該類實例并把它賦值到靜態(tài)變量中,如果我們不需要這個實例,它就永遠不會產(chǎn)生。這個就是延遲實例化

  • 懶漢模式(線程不安全)
public class Singleton {
    private static Singleton INSTANCE;

    private Singleton() {
    }

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

這段代碼簡單明了,而且使用了懶加載模式,但是卻存在致命的問題。當有多個線程并行調(diào)用 getInstance() 的時候,就會創(chuàng)建多個實例。也就是說在多線程下不能正常工作。

  • 懶漢模式(線程安全)
    解決懶漢模式線程安全問題,最簡單的方法是將整個 getInstance() 方法設為同步synchronized。
public class Singleton {
    private static Singleton INSTANCE;

    private Singleton() {
    }

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

雖然做到了線程安全,并且解決了多實例的問題,但是它并不高效。因為在任何時候只能有一個線程調(diào)用 getInstance()方法。但是同步操作只需要在第一次調(diào)用時才被需要,即第一次創(chuàng)建單例實例對象時。這就引出了雙重檢驗鎖。

  • 雙重校驗鎖 *
    雙重檢驗鎖模式(double checked locking pattern),是一種使用同步塊加鎖的方法。程序員稱其為雙重檢查鎖,因為會有兩次檢查 instance == null,一次是在同步塊外,一次是在同步塊內(nèi)。為什么在同步塊內(nèi)還要再檢驗一次?因為可能會有多個線程一起進入同步塊外的 if,如果在同步塊內(nèi)不進行二次檢驗的話就會生成多個實例了。
 public static Singleton getInstance() {
        if (INSTANCE == null) {  // 一重
            synchronized (Singleton.class) {
                if (INSTANCE == null) { // 二重
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }

這段代碼會有一個隱藏問題,主要是在INSTANCE = new Singleton()這句涉及到了JVM編譯器的指令重排,這并非是一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情:

  1. 給 instance 分配內(nèi)存
  2. 調(diào)用 Singleton 的構造函數(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 static volatile Singleton INSTANCE;

    private Singleton() {
    }
    /**
     * 雙重校驗鎖
     */
    public static Singleton getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }
}

關于volatile修飾符用最簡單的方式理解就是阻止了變量訪問前后的指令重排,保證了指令執(zhí)行順序。

  • 餓漢模式
public class Singleton {  
    private static final Singleton instance = new Singleton();  
    private Singleton (){}  
    public static Singleton getInstance() {  
        return instance;  
    }  
}  

這種方法非常簡單,因為單例的實例被聲明成 staticfinal 變量了,在第一次加載類到內(nèi)存中時就會初始化,所以創(chuàng)建實例本身是線程安全的。

這種方式基于classloder機制避免了多線程的同步問題,不過,instance在類裝載時就實例化,雖然導致類裝載的原因有很多種,在單例模式中大多數(shù)都是調(diào)用getInstance方法, 但是也不能確定有其他的方式(或者其他的靜態(tài)方法)導致類裝載,這時候初始化instance顯然沒有達到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;
    }

注意:

  1. 從外部無法訪問靜態(tài)內(nèi)部類SingletonHolder,只有當調(diào)用Singleton.getInstance方法的時候,才能得到單例對象INSTANCE。
  2. INSTANCE對象初始化的時機并不是在單例類Singleton被加載的時候,而是在調(diào)用getInstance方法,使得靜態(tài)內(nèi)部類SingletonHolder被加載的時候。因此這種實現(xiàn)方式是利用classloader的加載機制來實現(xiàn)懶加載,并保證構建單例的線程安全。
  3. 無法防止利用反射重復構建對象
  • 枚舉高效寫法
    在《Effective Java》最后推薦了這樣一個寫法,簡直有點顛覆,不僅超級簡單,而且保證了線程安全。這里引用一下,此方法無償提供了序列化機制,絕對防止多次實例化,及時面對復雜的序列化或者反射攻擊。單元素枚舉類型已經(jīng)成為實現(xiàn)Singleton的最佳方法。
public enum Singleton {
    /**
     *
     */
    INSTANCE;

    /**
     *
     */
    public void hello() {
        System.out.println("Hello World");
    }
}

對于一個標準的enum單例模式,最優(yōu)秀的寫法還是實現(xiàn)接口的形式:

public enum Singleton implements MySingleton {
    /**
     *
     */
    INSTANCE {
        @Override
        public void hello() {
            System.out.println("Hello world");
        }
    }
}

interface MySingleton {
    /**
     * xx
     */
    void hello();
}

使用枚舉實現(xiàn)單例模式不僅防止了反射構建對象也保證了線程安全,但是同時它并不是懶加載,在枚舉類加載的同時,其單例對象就已經(jīng)被初始化。

總結(jié)

單例模式寫法總結(jié)起來可以分為五種懶漢惡漢、雙重校驗鎖枚舉、靜態(tài)內(nèi)部類,上述所說都是線程安全的實現(xiàn),第一種應該說是不正確的實現(xiàn)。
對于這幾種的比較

單例模式 是否線程安全 是否懶加載 是否防止反射構建
雙重校驗鎖
枚舉
靜態(tài)內(nèi)部類

補充

  1. volatile關鍵字不但可以防止指令重排,也可以保證線程訪問的變量值是主內(nèi)存中的最新值。有關volatile的詳細原理,我在以后的漫畫中會專門講解。
  2. 使用枚舉實現(xiàn)的單例模式,不但可以防止利用反射強行構建單例對象,而且可以在枚舉類對象被反序列化的時候,保證反序列的返回結(jié)果是同一對象。
    對于其他方式實現(xiàn)的單例模式,如果既想要做到可序列化,又想要反序列化為同一對象,則必須實現(xiàn)readResolve方法。
  3. 應該在任何情況下都應實現(xiàn)線程安全的寫法。
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

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

  • 確保某一個類只有一個實例,而且自行實例化并向整個系統(tǒng)提供這個實例。 單例模式的使用很廣泛,比如:線程池(threa...
    richy_閱讀 383評論 0 0
  • 微信原文:設計模式 | 單例模式及典型應用 單例是最常見的設計模式之一,實現(xiàn)的方式非常多,同時需要注意的問題也非常...
    小旋鋒的簡書閱讀 1,865評論 2 5
  • 目錄 本文的結(jié)構如下: 什么是單例模式 為什么要用該模式 模式的結(jié)構 代碼示例 優(yōu)點和缺點 適用環(huán)境 模式應用 總...
    w1992wishes閱讀 487評論 1 2
  • 單例模式(Singleton Pattern)是眾多設計模式中較為簡單的一個,同時它也是面試時經(jīng)常被提及的問題,如...
    廖少少閱讀 655評論 0 1
  • 設計模式-單例模式 單例模式在網(wǎng)上已經(jīng)是被寫爛的一種設計模式了,筆者也看了不少的有關單例模式的文章,但是在實際生產(chǎn)...
    醒著的碼者閱讀 823評論 1 2

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