單例模式

前言

HI,歡迎來到《每周一博》。今天是十月第五周,我給大家簡單介紹一下單例模式。

單例模式是最簡單的模式,也是應用最廣的模式之一。有時整個系統只需要一個全局對象,這樣有利于協調系統整體的行為,比如硬件資源,數據庫等,這種不能由用戶自由創建對象的場景,就適合使用單例模式。

單例設計模式的優點:

  1. 在內存中只有一個實例,減少了內存開支,尤其是當一個對象需要頻繁創建銷毀,并且創建或銷毀時性能無法優化,單例模式的優勢就非常明顯;
  2. 單例模式只生成了一個實例,減少了系統性能開銷,當一個對象的產生需要較多資源時,如讀取配置,產生其他依賴對象時,可以通過應用啟動時直接產生一個單例對象,然后永久駐留內存的方式來解決;
  3. 單例模式可以避免對資源的多重占用,比如避免對同一個文件同時進行寫操作;
  4. 單例模式可以在系統設置全局的訪問點,優化和共享資源訪問,比如設計一個單例類,負責所有數據表的映射處理,安卓源碼的系統服務就是這么用的;

單例設計模式的缺點:

  1. 沒有抽象層接口,擴展困難,只能修改代碼,不適用于變化對象;
  2. 如果持有Context引用要使用Application Context,否則會造成內存泄漏;

實現單例模式需要注意的地方:
A. 保證構造函數是私有的;
B. 對外提供一個靜態方法來返回對象;
C. 要保證線程安全;
D. 反序列化也要保證對象唯一;

單例模式的實現主要有3種:

  1. 餓漢式:
    直接創建對象
// 直接創建靜態變量
public class Singleton {
     private static Singleton instance = new Singleton();
     private Singleton(){}
     public static synchronized Singleton getInstance(){
         return instance;
     }
 }

// 或者通過靜態初始化塊
public class Singleton {
    private static Singleton instance ;
    static {
        instance = new Singleton();
    }
    private Singleton(){}
    public static synchronized Singleton getInstance(){
        return instance;
    }
}

優點:實現簡單,線程安全;
缺點:不管該類有沒有用到都會創建對象,消耗資源

  1. Double CheckLock:
    線程安全的懶漢式,對象需要時才進行創建
public class Singleton {
    private static volatile Singleton instance;
    private Singleton(){}
    public static synchronized Singleton getInstance(){
        if(instance == null){
            synchronized (Singleton.class){
                if(instance ==null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

該實現方式會增加同步鎖來保證線程安全,注意先判空,再同步鎖,再判空,這樣就能夠做到效率和安全的雙重保證。那么為什么要進行2次判空呢?

new一個對象并不是一個原子操作,它會編譯成多條匯編指令,主要做了3件事:

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

由于Java編譯器允許處理器亂序執行,所以第2步和第3步的先后順序是不確定的,當兩個線程同時到達后就可能會創建2個實例。

Java語言提供了一種稍弱的同步機制,即volatile變量,用來確保將變量的更新操作通知到其他線程。當把變量聲明為volatile類型后,編譯器與運行時都會注意到這個變量是共享的,因此不會將該變量上的操作與其他內存操作一起重排序。volatile變量不會被緩存在寄存器或者對其他處理器不可見的地方,因此在讀取volatile類型的變量時總會返回最新寫入的值。

在訪問volatile變量時不會執行加鎖操作,因此也就不會使執行線程阻塞,因此volatile變量是一種比sychronized關鍵字更輕量級的同步機制。

  1. 靜態內部類:
    利用靜態類只會加載一次的機制,使用靜態內部類持有單例對象,達到單例的效果
public class Singleton {
    private Singleton(){}
    public static synchronized Singleton getInstance(){
        return SingletonHolder.instance;
    }
    private static class SingletonHolder{
        private static Singleton instance = new Singleton();
    }
}

靜態內部類的優點:
外部類加載時并不需要立即加載內部類,內部類不被加載則不去初始化instance,故而不占內存。即當SingleTon第一次被加載時,并不需要去加載SingletonHolder,只有當getInstance()方法第一次被調用時,才會去初始化instance,第一次調用getInstance()方法會導致虛擬機加載SingletonHolder類,這種方法不僅能確保線程安全,也能保證單例的唯一性,同時也延遲了單例的實例化。由于不用同步鎖機制,性能也會有所提升。

那靜態內部類又是如何實現線程安全的呢?我們先了解下類的加載時機。JAVA虛擬機在有且僅有的5種場景下會對類進行初始化:

  1. new一個關鍵字或者一個實例化對象;
    讀取或設置一個靜態字段時(final修飾,已在編譯期把結果放入常量池的除外);
    調用一個類的靜態方法時;
  2. 對類進行反射調用的時候,如果類沒進行初始化,需要先調用其初始化方法進行初始化;
  3. 初始化一個類時,如果其父類還未進行初始化,會先觸發其父類的初始化;
  4. 當虛擬機啟動時,用戶需要指定一個要執行的主類(包含main()方法的類),虛擬機會先初始化這個類;
  5. 當使用動態語言支持時,如果一個MethodHandle實例最后的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化;

這5種情況被稱為是類的主動引用,除此之外的所有引用類都不會對類進行初始化,稱為被動引用,靜態內部類就屬于被動引用的行列。

我們再看getInstance()方法,取的是SingletonHolder里的instance對象,跟上面那個DCL方法不同的是,getInstance()方法并沒有多次去new對象,故不管多少個線程去調用getInstance()方法,取的都是同一個instance對象,而不用去重新創建。當getInstance()方法被調用時,SingletonHolder才在SingletonHolder的運行時常量池里,把符號引用替換為直接引用,這時靜態對象instance也真正被創建,然后再被getInstance()方法返回出去。那么instance在創建過程中又是如何保證線程安全的呢?

虛擬機會保證一個類的clinit方法在多線程環境中被正確地加鎖、同步,如果多個線程同時去初始化一個類,那么只會有一個線程去執行這個類的clinit方法,其他線程都需要阻塞等待,直到活動線程執行clinit方法完畢。如果在一個類的clinit方法中有耗時很長的操作,就可能造成多個進程阻塞(需要注意的是,其他線程雖然會被阻塞,但如果執行clinit方法后,其他線程喚醒之后不會再次進入clinit方法。同一個加載器下,一個類型只會初始化一次),在實際應用中,這種阻塞往往是很隱蔽的。

故而,可以看出INSTANCE在創建過程中是線程安全的,所以說靜態內部類形式的單例可保證線程安全,也能保證單例的唯一性,同時也延遲了單例的實例化。

靜態內部類實現的缺點:
傳遞參數問題,由于是靜態內部類的形式去創建單例的,故外部無法傳遞參數進去,所以,我們創建單例時,可以在靜態內部類與DCL模式下權衡。

最后總結:

以上三種實現方式都是可以達到線程安全的效果,我們推薦使用靜態內部類的方式,另外還有枚舉也可以實現單例,它不僅能避免多線程同步問題,而且還能防止反射或反序列化重新創建新的對象,但是安卓官方不推薦,因為內存消耗是靜態常量的兩倍。

public enum  EnumMode {
    INSTANCE;
}

上述三種實現方式都需要考慮到反序列化的問題,如果Singleton實現了Serializable接口,那么這個類的實例就可能被序列化和復原。不管怎樣,如果序列化一個單例類的對象,那么復原多個之后就會有多個單例類的實例,因為需要重寫readResolve()方法,在該方法里返回instance,防止反序列化得到多個對象。

  private Object readResolve(){
        return instance;
    }

結尾:

本周給大家簡單介紹了單例模式的常見三種實現方式,其中雙重鎖和靜態內部類兩種方式涉及到了Java虛擬機創建對象的過程,這些內容還需要深入了解下。感謝大家的閱讀,我們下周再見。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容