前言
單例模式是最常用到的設計模式之一,熟悉設計模式的朋友對單例模式都不會陌生。一般介紹單例模式都只會提到餓漢式和懶漢式這兩種實現方式。
看完本章後,你可能會發現項目中的並沒有正確的使用創建單例,本文會將單例模式的創建方式和優缺點詳細描述。
一、單例模式介紹
單例模式(Singleton Pattern)是 Java 中最簡單的設計模式之一。這種類型的設計模式屬於創建型模式,它提供了一種創建對象的最佳方式。
這種模式涉及到一個單一的類,該類負責創建自己的對象,同時確保只有單個對象被創建。這個類提供了一種訪問其唯一的對象的方式,可以直接訪問,不需要實例化該類的對象。
很多時候整個系統只需要擁有一個的全局對象,這樣有利於我們協調系統整體的行爲。比如在某個服務器程序中,該服務器的配置信息存放在一個文件中,這些配置數據由一個單例對象統一讀取,然後服務進程中的其他對象再通過這個單例對象獲取這些配置信息。這種方式簡化了在複雜環境下的配置管理。
二、單例模式的特點
單例模式具備以下特點:
- 單例類只能有一個實例。
- 單例類必須自己創建自己的唯一實例。
- 單例類必須給所有其他對象提供這一實例。
優點:由於單例模式只生成了一個實例,所以能夠節約系統資源,減少性能開銷,提高系統效率,同時也能夠嚴格控制客戶對它的訪問。
缺點:也正是因爲系統中只有一個實例,這樣就導致了單例類的職責過重,違背了“單一職責原則”,同時也沒有抽象類,這樣擴展起來有一定的困難。
三、單例模式的實現
單例模式要求類能夠有返回對象一個引用(永遠是同一個)和一個獲得該實例的方法(必須是靜態方法,通常使用 getInstance 這個名稱)。
單例的實現主要是通過以下兩個步驟:
- 將該類的構造方法定義爲私有方法,這樣其他處的代碼就無法通過調用該類的構造方法來實例化該類的對象,只有通過該類提供的靜態方法來得到該類的唯一實例;
- 在該類內提供一個靜態方法,當我們調用這個方法時,如果類持有的引用不爲空就返回這個引用,如果類保持的引用爲空就創建該類的實例並將實例的引用賦予該類保持的引用。
1、餓漢模式(線程安全)
public class Singleton {
private final static Singleton INSTANCE = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return INSTANCE;
}
}
- 優點:這種寫法比較簡單,就是在類裝載的時候就完成實例化。避免了線程同步問題。
- 缺點:在類裝載的時候就完成實例化,沒有達到 Lazy Loading 的效果。如果從始至終從未使用過這個實例,則會造成內存的浪費。
2、懶漢模式(線程不安全)
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
這種寫法起到了 Lazy Loading 的效果,但是隻能在單線程下使用。如果在多線程下,一個線程進入了 if (singleton == null)判斷語句塊,還未來得及往下執行,另一個線程也通過了這個判斷語句,這時便會產生多個實例。所以在多線程環境下不可使用這種方式。
3、懶漢式(線程安全)
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
解決線程不安全問題,做個線程同步就可以了,於是就對 getInstance()方法進行了線程同步。
缺點:效率太低了,每個線程在想獲得類的實例時候,執行 getInstance()方法都要進行同步。而其實這個方法只執行一次實例化代碼就夠了,後面的想獲得該類實例,直接 return 就行了。方法進行同步效率太低要改進。
4、靜態內部類(線程安全)
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 的實例化。
優點:避免了線程不安全,延遲加載,效率高。
5、枚舉(線程安全)
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
藉助 JDK1.5 中添加的枚舉來實現單例模式。不僅能避免多線程同步問題,而且還能防止反序列化重新創建新的對象。可能是因爲枚舉在 JDK1.5 中才添加,所以在實際項目開發中,很少見人這麼寫過。
6、雙重校驗鎖法(線程安全)
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
Double-Check 概念對於多線程開發者來說不會陌生,如代碼中所示,我們進行了兩次 if (singleton == null)檢查,這樣就可以保證線程安全了。這樣,實例化代碼只用執行一次,後面再次訪問時,判斷 if (singleton == null),直接 return 實例化對象。
優點:線程安全;延遲加載;效率較高。
四、單例模式注意事項
1、內存泄漏
單例模式在 Android 開發中會經常用到,但是如果使用不當就會導致內存泄露。因爲單例的靜態特性使得它的生命週期同應用的生命週期一樣長,如果一個對象已經沒有用處了,但是單例還持有它的引用,那麼在整個應用程序的生命週期它都不能正常被回收,從而導致內存泄露。
public class Singleton {
private static Singleton singleton = null;
private Context mContext;
public Singleton(Context mContext) {
this.mContext = mContext;
}
public static Singleton getSingleton(Context context){
if (null == singleton){
singleton = new Singleton(context);
}
return singleton;
}
}
調用 getInstance(Context context)方法的時候傳入的 context 參數是 Activity、Service 等上下文,就會導致內存泄露。
當我們退出 Activity 時,該 Activity 就沒有用了,但是因爲 singleton 作爲靜態單例(在應用程序的整個生命週期中存在)會繼續持有這個 Activity 的引用,導致這個 Activity 對象無法被回收釋放,這就造成了內存泄露。
爲了避免這樣單例導致內存泄露,我們可以將 context 參數改爲全局的上下文:
public Singleton(Context mContext) {
this.mContext = mContext.getApplicationContext();
}
全局的上下文 Application Context 就是應用程序的上下文,和單例的生命週期一樣長,這樣就避免了內存泄漏。單例模式對應應用程序的生命週期,所以我們在構造單例的時候儘量避免使用 Activity 的上下文,而是使用 Application 的上下文。
2、線程安全
單例模式在多線程的應用場合下必須小心使用。如果當唯一實例尚未創建時,有兩個線程同時調用創建方法,那麼它們同時沒有檢測到唯一實例的存在,從而同時各自創建了一個實例,這樣就有兩個實例被構造出來,從而違反了單例模式中實例唯一的原則。解決這個問題的辦法是爲指示類是否已經實例化的變量提供一個互斥鎖(但是這樣會降低效率)。
- volatile 關鍵字
Volatile 變量具有 synchronized 的可見性特性,但是不具備原子特性。這就是說線程能夠自動發現 volatile 變量的最新值。Volatile 變量可用於提供線程安全,但是隻能應用於非常有限的
- volatile 關鍵字作用
-
防止指令重排:規定了 volatile 變量不能指令重排,必須先寫再讀。
-
內存可見:線程從內存中讀取 volatile 修飾的變量的數據,直接從主內存中獲取數據,不需要經過 CPU 緩存,這樣使得多線程獲取的數據都是一致的。如圖所示:
- volatile 和 synchronized 區別
volatile 不能夠替代 synchronized,原因有兩點:
- 對於多線程,不是一種互斥關係
- 不能保證變量狀態的“原子性操作”,所以 volatile 不能保證原子性問題
3、序列化傳遞數據
我們期望單例模式可以保證只創建一個實例,而通過特殊手段創建出其他的實例,就對單例模式造成了破壞。反序列化就會破壞單例模式。
單例實現了 serializable 接口,反序列化時會通過反射調用無參構造方法創建一個新的實例,這時就要重寫 readResolve 方法規避序列化破壞單例,如下:
//防止序列化破壞單例模式
public Object readResolve() {
return SingletonHolder.INSTANCE;
}
而枚舉在序列化的時候僅是將枚舉對象的 name 屬性輸出到結果中,反序列化時通過 java.lang.Enum 的 valueOf 方法根據 name 查找枚舉對象。同時,編譯器是不允許任何對這種序列化機制的定製的,因此禁用了 writeObject、readObject、readObjectNoData、writeReplace 和 readResolve 等方法。
也就是說,枚舉的反序列化不是通過反射實現的,所以不會破壞單例模式。
原則上不允許用單例模式序列化傳遞數據,如果一定要這麼做,請考慮數據恢復現場。
五、總結
一般來說,單例模式有五種寫法:懶漢、餓漢、雙重檢驗鎖、靜態內部類、枚舉。
開發過程中,一般情況下直接使用餓漢式就好了,如果明確要求要懶加載(lazy initialization)傾向於使用靜態內部類。如果涉及到反序列化創建對象時會試着使用枚舉的方式來實現單例。
一個單例模式,涉及到的知識點包括了線程安全,類加載機制,枚舉實現原理,序列化,反射等多個知識點。即使是已經學過,仍然有回顧的價值。
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對的支持。