常用代碼整理:單例模式的生產環境用法

說明:大部分內容都是參考別的文章,這裏做整理是爲了以後的編程有實用的模板,可以即需即用。

對於一些類來說,只有一個實例是很重要的。要怎樣才能保證一個類只有一個實例並且這個實例易於被訪問呢?一個全局變量使得一個對象可以被訪問,但它不能防止你實例化多個對象。一個更好的方法是,讓類自身負責保存它的唯一實例。這個類可以保證沒有其他實例可以被創建(通過截取創建新對象的請求),並且它可以提供一個訪問該實例的方法。這就是單例模式(Singleton)。1

常見的幾種單例模式:

1、懶漢式(線程不安全)

這種寫法 lazy loading 很明顯,但在多線程不能正常工作,嚴格意義上並不算單例模式。

public class Singleton {
    private static Singleton instance;

    private Singleton(){ }

    public static Singleton getInstance(){
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
2、懶漢式(線程安全)

這種寫法能夠在多線程中很好的工作,而且看起來它也具備很好的 lazy loading,但效率很低,99% 情況下不需要同步。
優點:第一次調用才初始化,避免內存浪費。
缺點:必須加鎖 synchronized 才能保證單例,但加鎖會影響效率。

public class Singleton {
    private static Singleton instance;

    private Singleton(){ }

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

只有在第一次執行此方法時(getInstance()),才真正需要同步。換句話說,一旦設置好 instance 對象,就不再需要同步這個方法了。之後每次調用這個方法,同步都是一種累贅。爲了符合大多數 Java 應用程序,很明顯地,我們需要確保單例模式能在多線程的狀況下正常工作。但是似乎同步 getInstance() 的做法將拖垮性能,而 “雙重校驗鎖” 可以解決這個問題。2

3、餓漢式【推薦使用

這種方式基於 classloder 機制避免了多線程的同步問題,不過,instance 在類裝載時就實例化,這時候初始化 instance 顯然沒有達到 lazy loading 的效果。
描述:這種方式比較常用,但容易產生垃圾對象。
優點:沒有加鎖,執行效率會提高。
缺點:類加載時就初始化,浪費內存。

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton(){ }

    public static Singleton getInstance(){
        return INSTANCE;
    }
}

首先 final 關鍵詞是代表此變量一經賦值,其指向的內存引用地址將不會再改變。聲明爲 final 的變量,必須在類加載完成時已經賦值,如果你是 final 非 static 成員,必須在構造器、代碼塊或者在直接定義時賦值;如果是 final static 成員變量,必須在直接定義時或者在靜態代碼塊中賦值。然而,在直接定義時賦值或者在靜態代碼塊中賦值就變成餓漢模式了,所以懶漢模式中,不能用 final 修飾。

4、餓漢式(變種)

表面上看起來差別挺大,其實跟第三種方式差不多,都是在類初始化即實例化 instance。

// 變種1
public class Singleton {
    private static final Singleton instance = null;

    static{
        instance = new Singleton();
    }

    private Singleton(){ }

    public static Singleton getInstance(){
        return instance;
    }
}
// 變種2
public class Singleton {
    public static final Singleton instance = new Singleton();
    private Singleton(){ }
}
5、靜態內部類(登記式,既能夠實現延遲加載,又能夠實現線程安全)【強烈推薦使用

這種方式同樣利用了 classloder 的機制來保證初始化 instance 時只有一個線程,它跟第三種和第四種方式不同的是(很細微的差別):第三種和第四種方式是隻要 Singleton 類被裝載了,那麼 instance 就會被實例化(沒有達到 lazy loading 效果),而這種方式是 Singleton 類被裝載了,instance 不一定被初始化。因爲 SingletonHolder 類沒有被主動使用,只有顯示通過調用 getInstance 方法時,纔會顯示裝載 SingletonHolder 類,從而實例化 instance。想象一下,如果實例化 instance 很消耗資源,想讓它延遲加載,另外一方面,不希望在 Singleton 類加載時就實例化,因爲不能確保 Singleton 類在其他的地方被主動使用從而被加載,那麼這個時候實例化 instance 顯然是不合適的。這個時候,這種方式相比第三和第四種方式就顯得很合理。

public class Singleton {

    private static class SingletonHolder{
        private static final Singleton INSTANCE = new Singleton();
    }

    private Singleton(){ }

    // static method declared final.
    public static Singleton getInstance(){
        return SingletonHolder.INSTANCE;
    }
}

(1)相應的基礎知識

  • 什麼是類級內部類:簡單點說,類級內部類指的是,有 static 修飾的成員內部類。如果沒有 static 修飾的成員式內部類被稱爲對象級內部類。
  • 類級內部類相當於其外部類的 static 成分,它的對象與外部類對象間不存在依賴關係,因此可以直接創建。而對象級內部類的實例,是綁定在外部對象實例中的。
  • 類級內部類中,可以定義靜態的方法。在靜態方法中只能引用外部類中的靜態成員方法或變量。
  • 類級內部類相當於其外部類的成員,只有在第一次被使用的時候纔會被裝載。【內部類(不論是靜態內部類還是非靜態內部類)都是在第一次使用時纔會被加載。 對於非靜態內部類是不能出現靜態模塊(包含靜態塊,靜態屬性,靜態方法等) 。非靜態類的使用需要依賴於外部類的對象。】

(2)多線程缺省同步鎖的知識

在多線程開發中,爲了解決併發問題,可以通過使用 synchronized 來加互斥鎖進行同步控制,但是在某些情況下,JVM 已經隱含的爲我們執行了同步,這些情況下就不用我們再來進行同步控制了。這些情況包括:

  • 由靜態初始化器(在靜態字段上或 static{} 塊中的初始化器)初始化數據時
  • 訪問 final 字段時
  • 在創建線程之前創建對象時
  • 線程可以看見它將要處理的對象時

(3)對類進行初始化
虛擬機規範嚴格規定了有且只有四種情況必須立即對類進行初始化:遇到 new、getStatic、putStatic 或 invokeStatic 這4條字節碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。 生成這4條指令最常見的java代碼場景是:

  • 使用 new 關鍵字實例化對象
  • 讀取一個類的靜態字段(被 final 修飾、已在編譯期把結果放在常量池的靜態字段除外)
  • 設置一個類的靜態字段(被 final 修飾、已在編譯期把結果放在常量池的靜態字段除外)
  • 調用一個類的靜態方法
6、枚舉

這種方式是 Effective Java 作者 Josh Bloch 提倡的方式:

【.. xlh …?】這種方法在功能上與公有域方法相近,但它更簡潔,無償地提供了序列化機制,絕對防止多次實例化,即使是在面對複雜的序列化或者反射攻擊的時候。雖然這種方法還沒有廣泛採用,但是單元素的枚舉類型已經成爲實現 Singleton 的最佳方法。3

它不僅能避免多線程同步問題,而且還能防止反序列化重新創建新的對象,可謂是很堅強的壁壘啊,不過,個人(某博主)認爲由於 JDK1.5 中才加入 enum 特性,用這種方式寫不免讓人感覺生疏,在實際工作中,也很少看見有人這麼寫過。這種實現方式還沒有被廣泛採用,但這是實現單例模式的最佳方法。它更簡潔,自動支持序列化機制,絕對防止多次實例化。如果涉及到反序列化創建對象時,可以嘗試使用第 6 種枚舉方式。

public enum Singleton {
    INSTANCE;
    public void whateverMethod(){
        ...
    }
}
7、雙重校驗鎖

這個是第二種方式的升級版,俗稱雙重檢查鎖定。在 JDK1.5 之後,雙重檢查鎖定才能夠正常達到單例效果。這種方式採用雙鎖機制,安全且在多線程情況下能保持高性能。

public class Singleton {
    private volatile static Singleton instance;

    private Singleton(){ }

    public static Singleton getInstance(){
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

Java volatile 關鍵字:當一個共享變量被 volatile 修飾時,它會保證修改的值立即被更新到主存;內存可見性,通俗來說就是,線程 A 對一個 volatile 變量的修改,對於其它線程來說是可見的,即線程每次獲取 volatile 變量的值都是最新的。

要特別注意使用單例模式時,可能會出現的內存泄漏問題:單例的靜態特性使得單例的生命週期和應用的生命週期一樣長,如果一個對象已經不再使用了,而單例對象持有對象的引用,那麼這個對象將不能被正常回收,這就導致了內存泄漏。

再特別說一句:由於 Dialog 比較特別,只能用 Activity 才能啓動(當然有一些用其他 context 也可以,但是要申請權限什麼的,不適用於一般app),所以不要使用單例 Dialog 工具類,在 Activity 或 Fragment 的生命中控制 Dialog 的 show 和 dismiss。

【Context】 Application Activity Service ContentProvider BroadcastReceiver
Show a Dialog NO YES NO NO NO
Start an Activity NO1 YES NO1 NO1 NO1
Layout Inflation NO2 YES NO2 NO2 NO2
Start a Service YES YES YES YES YES
Bind to a Service YES YES YES YES NO
Send a Broadcast YES YES YES YES YES
Register BroadcastReceiver YES YES YES YES NO3
Load Resource Values YES YES YES YES YES
  • NO1 表示 Application context 的確可以開始一個 Activity,但是它需要創建一個新的 task。這可能會滿足一些特定的需求,但是在你的應用中會創建一個不標準的回退棧(back stack),這通常是不推薦的或者不是好的實踐。
  • NO2 表示這是非法的,但是這個填充(inflation)的確可以完成,但是是使用所運行的系統默認的主題(theme),而不是你 app 定義的主題。
  • NO3 在 Android4.2 以上,如果 Receiver 是 null 的話(這是用來獲取一個 sticky broadcast 的當前值的),這是允許的。

參考文章:
1、http://cantellow.iteye.com/blog/838473
2、http://www.runoob.com/design-pattern/singleton-pattern.html
3、https://blog.csdn.net/qq_33277870/article/details/77967532
4、https://blog.csdn.net/jason0539/article/details/23297037/
5、http://blog.sina.com.cn/s/blog_6d2890600101gb8x.html
6、https://q.cnblogs.com/q/DetailPage/95556/
7、https://blog.csdn.net/qq_32618417/article/details/51703414
8、https://www.jianshu.com/p/c87677f01ed5
9、https://www.jianshu.com/p/413ec659500a
10、https://blog.csdn.net/race604/article/details/9331807


  1. 《設計模式:可複用面向對象軟件的基礎》
  2. 《Head First 設計模式》
  3. 《Effective Java》
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章