孔乙己的疑問:單例模式有幾種寫法

引子

單例模式的文章能夠說是百家爭鳴,今天我也來講道說道,你們共同提高。html

單例模式的做用和使用場景

單例模式(Singleton Pattern)

確保某一個類只有一個實例,並且能夠自行實例化並向整個系統提供這個實例,這個類稱爲單例類,它提供全局訪問的方法。 單例模式是一種對象建立型模式。數據庫

使用場景

好比一個應用中應該只存在一個ImageLoader實例。設計模式

Android中的LayoutInflater類等。安全

EventBus中getDefault()方法獲取實例。bash

保證對象惟一
  1. 爲了不其餘程序過多創建該類對象。先禁止其餘程序創建該類對象
  2. 還爲了讓其餘程序能夠訪問到該類對象,只好在本類中,自定義一個對象。
  3. 爲了方便其餘程序對自定義對象的訪問,能夠對外提供一些訪問方式。

這三步怎麼用代碼體現呢?微信

  1. 將構造函數私有化。
  2. 在類中建立一個本類對象。
  3. 提供一個方法能夠獲取到該對象。

單例模式的十二種寫法

1、餓漢式(靜態變量)
public class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton() {
    }
    public static Singleton getInstance() {
        return instance;
    }
}
複製代碼
2、餓漢式(靜態常量)
public class Singleton {
    private final static Singleton INSTANCE = new Singleton();
    private Singleton() {
    }
    public static Singleton getInstance() {
        return INSTANCE;
    }
}

複製代碼
3、餓漢式(靜態代碼塊)
public class Singleton {
    private static Singleton instance;
    static {
        instance = new Singleton();
    }
    private Singleton() {
    }
    public static Singleton getInstance() {
        return instance;
    }
}

複製代碼

上面三種寫法本質上實際上是同樣的,也是各種文章在介紹餓漢式時經常使用的方式。但使用靜態final的實例對象或者使用靜態代碼塊依舊不能解決在反序列化、反射、克隆時從新生成實例對象的問題。多線程

序列化:一是能夠將一個單例的實例對象寫到磁盤,實現數據的持久化;二是實現對象數據的遠程傳輸。 當單例對象有必要實現 Serializable 接口時,即便將其構造函數設爲私有,在它反序列化時依然會經過特殊的途徑再建立類的一個新的實例,至關於調用了該類的構造函數有效地得到了一個新實例!併發

反射:能夠經過setAccessible(true)來繞過 private 限制,從而調用到類的私有構造函數建立對象。框架

克隆:clone()是 Object 的方法,每個對象都是 Object 的子類,都有clone()方法。clone()方法並非調用構造函數來建立對象,而是直接拷貝內存區域。所以當咱們的單例對象實現了 Cloneable 接口時,儘管其構造函數是私有的,仍能夠經過克隆來建立一個新對象,單例模式也相應失效了。函數

優勢:寫法比較簡單,在類裝載的時候就完成實例化。避免了線程同步問題。

缺點:在類裝載的時候就完成實例化,沒有達到Lazy Loading的效果。若是從始至終從未使用過這個實例,則會形成內存的浪費。


那麼咱們就要考慮懶加載的問題了。

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

複製代碼

優勢:懶加載,只有使用的時候纔會加載。

缺點:可是隻能在單線程下使用。若是在多線程下,instacnce對象仍是空,這時候兩個線程同時訪問getInstance()方法,由於對象仍是空,因此兩個線程同時經過了判斷,開始執行new的操做。因此在多線程環境下不可以使用這種方式。

5、懶漢式(線程安全,存在同步開銷)
public class Singleton {
    private static Singleton instance;
    private Singleton() {
    }
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

複製代碼

優勢:懶加載,只有使用的時候纔會加載,獲取單例方法加了同步鎖,保正了線程安全。

缺點:效率過低了,每一個線程在想得到類的實例時候,執行getInstance()方法都要進行同步。

6、懶漢式(線程僞裝安全,同步代碼塊)
public class Singleton {
    private static Singleton instance;
    private Singleton() {
    }
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                instance  = new Singleton();
            }
        }
        return instance;
    }
}

複製代碼

優勢:改進了第五種效率低的問題。

缺點:但實際上這個寫法還不能保證線程安全,和第四種寫法相似,只要兩個線程同時進入了 if (singleton == null) { 這句判斷,照樣會進行兩次new操做


接下來就是聽起來很牛逼的雙重檢測加鎖的單例模式。

7、DCL「雙重檢測鎖:Double Checked Lock」 單例(假)
public class Singleton {
    private static Singleton instance = null;
    private Singleton() {
    }
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance  = new Singleton();
                }
            }
        }
        return instance;
    }
}

複製代碼

本例的亮點都在getInstance()方法上,能夠看到在該方法中對instance進行了兩次判空:第一層判斷爲了不沒必要要的同步,第二層判斷則是爲了在null的狀況下建立實例。對第六種單例的漏洞進行了彌補,可是仍是有丶小問題的,問題就在instance = new Singleton();語句上。

這語句在這裏看起來是一句代碼啊,但實際上它並非一個原子操做,這句代碼最終會被編譯成多條彙編指令,它大體作了3件事情:

  1. 給Singleton的實例分配內存
  2. 調用Singleton()的 構造函數,初始化成員字段
  3. 將instance對象指向分配的內存空間(此時instance就不是null了)

可是,因爲Java編譯器運行處理器亂序執行,以及jdk1.5以前Java內存模型中Cache、寄存器到主內存會寫順序的規定,上面的第二和第三的順序是沒法保證的。也就是說,執行順序多是1-2-3也多是1-3-2.若是是後者,而且在3執行完畢、2未執行以前,被切換到線程B上,這時候instance由於已經在線程A內執行3了,instance已是非null,全部線程B直接取走instance,再使用時就會出錯,這就是DCL失效問題,並且這種難以跟蹤難以重現的問題極可能會隱藏好久。

優勢:線程安全;延遲加載;效率較高。

缺點:JVM編譯器的指令重排致使單例出現漏洞。

8、DCL「雙重檢測鎖:Double Checked Lock」 單例(真,推薦使用)
public class Singleton {
    private static volatile Singleton instance = null;
    private Singleton() {
    }
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance  = new Singleton();
                }
            }
        }
        return instance;
    }
}

複製代碼

在jdk1.5以後,官方已經注意到這種問題,調整了JVM、具體化了volatile關鍵字,所以,若是是1.5或以後的版本,只須要將instance的定義改爲private static volatile Singleton instance = null;

就能夠保證instance對象每次都是從主內存中讀取,就可使用DCL的寫法來完成單例模式。固然,volatile多少會影響到性能,但考慮到程序的正確性,犧牲這點性能仍是值得的。

優勢:線程安全;延遲加載;效率較高。

缺點:因爲volatile關鍵字會屏蔽Java虛擬機所作的一些代碼優化,略微的性能下降,但除非你的代碼在併發場景比較複雜或者低於JDK6版本下使用,不然,這種方式通常是可以知足需求的。

9、靜態內部類(推薦使用)
public class Singleton {
    private Singleton() {
    }
    private static class SingletonInstance {
        private static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance() {
        return SingletonInstance.INSTANCE;
    }
}

複製代碼

這種方式跟餓漢式方式採用的機制相似,但又有不一樣。 二者都是採用了類裝載的機制來保證初始化實例時只有一個線程。不一樣的地方在餓漢式方式是隻要Singleton類被裝載就會實例化,沒有Lazy-Loading的做用,而靜態內部類方式在Singleton類被裝載時並不會當即實例化,而是在須要實例化時,調用getInstance方法,纔會裝載SingletonInstance類,從而完成Singleton的實例化。

因此在這裏,利用 JVM的 classloder 的機制來保證初始化 instance 時只有一個線程。JVM 在類初始化階段會獲取一個鎖,這個鎖能夠同步多個線程對同一個類的初始化

優勢:避免了線程不安全,延遲加載,效率高。

缺點:依舊不能解決在反序列化、反射、克隆時從新生成實例對象的問題。

10、枚舉
public enum Singleton {
    INSTANCE
}

複製代碼

枚舉類單例模式是《Effective Java》做者 Josh Bloch 極力推薦的單例方法

藉助JDK 1.5中添加的枚舉來實現單例模式。P.S. Enum是沒有clone()方法的

  1. 枚舉類類型是 final 的「不能夠被繼承」
  2. 構造方法是私有的「也只能私有,不容許被外部實例化,符合單例」
  3. 類變量是靜態的
  4. 沒有延時初始化,隨着類的初始化就初始化了「從上面靜態代碼塊中能夠看出」
  5. 由 4 能夠知道枚舉也是線程安全的

優勢:寫法簡單,不只能避免多線程同步問題,並且還能防止反序列化、反射,克隆從新建立新的對象。

缺點:JDK 1.5以後才能使用。

11、登記式單例--使用Map容器來管理單例模式
public class SingletonManger {
    private static Map<String, Object> objectMap = new HashMap<String, Object>();
    private SingletonManger() {
    }
    public static void registerService(String key, Object instance) {
        if (!objectMap.containsKey(key)) {
            objectMap.put(key, instance);
        }
    }
    public static Object getService(String key) {
        return objectMap.get(key);
    }
}

複製代碼

查閱Android源碼中的 LayoutInflater 對象就能發現使用了這種寫法

優勢:在程序的初始,將多種單例類型注入到一個統一的管理類中,在使用時根據key獲取對象對應類型的對象。這種方式使得咱們能夠管理多種類型的單例,而且在使用時能夠經過統一的接口進行獲取操做, 下降了用戶的使用成本,也對用戶隱藏了具體實現,下降了耦合度。

缺點:不經常使用,有些麻煩

12、內部枚舉類

在微信公衆號看到有大佬說使用枚舉配合內部類實現內部枚舉類,能夠達成線程安全,懶加載,責任單一原則,等等是如今最完美的寫法。


四種需求的知足狀況圖

總結

若是你和我同樣是Android開發,那麼因爲在客戶端一般沒有高併發的狀況,選擇哪一種實現方式並不會有太大的影響。但即使如此,出於效率考慮咱們也應該使用後面幾種單例方法。

單例模式的優勢

單例模式的優勢其實已經在定義中提現了:能夠減小系統內存開支,減小系統性能開銷,避免對資源的多重佔用、同時操做。

單例模式的缺點
  1. 違反了單一責任鏈原則,測試困難 單例類的職責太重,在必定程度上違背了「單一職責原則」。由於單例類既充當了工廠角色,提供了工廠方法,同時又充當了產品角色,包含一些業務方法,將產品的建立和產品的自己的功能融合到一塊兒。
  2. 擴展困難 因爲單例模式中沒有抽象層,所以單例類的擴展有很大的困難。修改功能必須修改源碼。
  3. 共享資源有可能不一致。 如今不少面嚮對象語言(如Java、C#)的運行環境都提供了自動垃圾回收的技術,所以,若是實例化的共享對象長時間不被利用,系統會認爲它是垃圾,會自動銷燬並回收資源,下次利用時又將從新實例化,這將致使共享的單例對象狀態的丟失。
注意在Application中存取數據

在Android 應用啓動後、任意組件被建立前,系統會自動爲應用建立一個 Application類(或其子類)的對象,且只建立一個。今後它就一直在那裏,直到應用的進程被殺掉。

因此雖然 Application並無採用單例模式來實現,可是因爲它的生命週期由框架來控制,和整個應用的保持一致,且確保了只有一個,因此能夠被看做是一個單例。 可是若是你直接用它來存取數據,那你將獲得無窮無盡的NullPointerException。

由於Application 不會永遠駐留在內存裏,隨着進程被殺掉,Application 也被銷燬了,再次使用時,它會被從新建立,它以前保存下來的全部狀態都會被重置。

要預防這個問題,咱們不能用 Application 對象來傳遞數據,而是要:

  1. 經過傳統的 intent 來顯式傳遞數據(將 Parcelable 或 Serializable 對象放入Intent / Bundle。Parcelable 性能比 Serializable 快一個量級,可是代碼實現要複雜一些)。

  2. 重寫onSaveInstanceState()以及onRestoreInstanceState()方法,確保進程被殺掉時保存了必須的應用狀態,從而在從新打開時能夠正確恢復現場。

  3. 使用合適的方式將數據保存到數據庫或硬盤。

  4. 老是作判空保護和處理。


參考文章

《Android 源碼設計模式解析與實戰》

www.cnblogs.com/zhaoyan001/…

www.jianshu.com/p/4f4f2fa7e…

www.jianshu.com/p/9b3587e8b…

相關文章
相關標籤/搜索