設計模式:如何優雅地手寫單例模式

單例模式是一種經常使用的設計模式,該模式提供了一種建立對象的方法,確保在程序中一個類最多隻有一個實例。java

單例有什麼用處?設計模式

有一些對象其實咱們只須要一個,好比線程池、緩存、對話框、處理偏好設置和註冊表的對象、日誌對象,充當打印機、顯示等設備的驅動程序對象。其實,這類對象只能有一個實例,若是製造出來多個實例,就會致使許多問題,如:程序的行爲異常、資源使用過量,或者是不一致的結果。緩存

Singleton一般用來表明那些本質上惟一的系統組件,好比窗口管理器或者文件系統。安全

在Java中實現單例模式,須要一個靜態變量、一個靜態方法和私有的構造器。多線程

經典的單例模式實現

對於一個簡單的單例模式,能夠這樣實現:併發

  1. 定義一個私有的靜態變量uniqueInstance;
  2. 定義私有的構造方法。這樣別處的代碼沒法經過調用該類的構造函數來實例化該類的對象,只能經過該類提供的靜態方法來獲得該類的惟一實例;函數

  3. 提供一個getInstance()方法,該方法中判斷是否已經存在該類的實例,若是存在直接返回,不存在則新建一個再返回。代碼以下:post

public class Singleton{
    private static Singleton uniqueInstance;//私有靜態變量
    
    //私有的構造器。這樣別處的代碼沒法經過調用該類的構造函數來實例化該類的對象,只能經過該類提供的靜態方法來獲得該類的惟一實例。
    private Singleton(){}
    
    //靜態方法
    public static Singleton getInstance(){
        //若是不存在,利用私有構造器產生一個Singleton實例並賦值到uniqueInstance靜態變量中。
        //若是咱們不須要這個實例,他就永遠不會產生。這叫作「延遲實例化(懶加載)「
        if(uniqueInstance == null){
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}

這段代碼使用了延遲實例化,在單線程中沒有任何問題。可是在多線程環境下,當有多個線程並行調用 getInstance(),都認爲uniqueInstance爲null的時候,就會調用uniqueInstance = new Singleton();,這樣就會建立多個Singleton實例,沒法保證單例。性能

解決多線程環境下的線程安全問題,主要有如下幾種寫法:線程

同步getInstance()方法

關鍵字synchronized能夠保證在他同一時刻,只有一個線程能夠執行某一個方法,或者某一個代碼塊。

同步getInstance()方法是處理多線程最直接的作法。只要把getInstance()變成同步(synchronized)方法,就能夠解決併發問題了。

public class Singleton{
    private static Singleton uniqueInstance;//私有靜態變量

    //私有構造器
    private Singleton() {}
    
    //synchronized同步方法
    public static synchronized Singleton getInstance(){
        if(uniqueInstance == null){
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}

可是,同步的效率低,會下降性能。只有第一次執行此方法的時候,才真正須要同步。也就是說,一旦設置好uniqueInstance變量,就再也不須要同步這個方法了。以後每次調用這個方法,同步都是一種累贅。同步getInstance()方法既簡單又有效。若是說對性能要求不高,這樣就能夠知足要求。

「急切」實例化

以前的實現採用的是懶加載方式,也就是說,當真正用到的時候纔會建立;若是沒被使用到,就一直不會建立。

懶加載方式在第一次使用的時候, 須要進行初始化操做,可能會比較耗時。

若是肯定一個對象必定會使用的話,能夠採用「急切」地實例化,事先準備好這個對象,須要的時候直接使用就好了。這種方式也叫作餓漢模式。具體代碼:

public class Singleton{
    //在靜態初始化器中建立單例,保證了線程安全性
    private static Singleton uniqueInstance = new Singleton();
    
    private Singleton() {}
    
    public static Singleton getInstance(){
        return uniqueInstance;
    }
}

餓漢模式是如何保證線程安全的?

餓漢模式中的靜態變量是隨着類加載時被初始化的。static關鍵字保證了該變量是類級別的,也就是說這個類被加載的時候被初始化一次。注意與對象級別和方法級別進行區分。

由於類的初始化是由類加載器完成的,這實際上是利用了類加載器的線程安全機制。類加載器的loadClass方法在加載類的時候使用了synchronized關鍵字。也正是由於這樣, 除非被重寫,這個方法默認在整個裝載過程當中都是同步的(線程安全的)。

雙重檢查加鎖

殺雞用牛刀。實現單例模式能夠利用雙重檢查加鎖(double-checked locking),首先檢查是否實例已經建立了,若是還沒有建立,「才」進行同步。這樣,只有第一次會同步。

public class Singleton{
    //使用volatile關鍵字,確保當uniqueInstance變量被初始化成爲Singleton實例時,多線程能夠正確地處理uniqueInstance變量。
    private volatile static Singleton uniqueInstance;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        if(uniqueInstance == null){//第一次檢查
            synchronized(Singleton.class){
                if(uniqueInstance == null){//第二次檢查
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
    
}

若是性能是關注的重點,雙重檢查加鎖能夠大幅減小getInstance()的時間消耗成本。

在Java 1.5發行版本以前,雙重檢查模式的功能很不穩定,由於volatile修飾符的語義不夠強,難以支持它。Java 1.5發行版本中引入的內存模式解決了這個問題,現在,雙重檢查模式是延遲初始化的一個實例域的方法。

爲何要進行雙重檢查?只檢查一次不行嗎?

解答:只檢查一次不行。只檢查一次的代碼以下:

if(uniqueInstance == null){//第一次檢查
            synchronized(Singleton.class){
                    uniqueInstance = new Singleton();
            }
        }

當兩個線程同時判斷uniqueInstance == null的時候,都會去得到Singleton.class的鎖對象,因爲兩個線程擁有的鎖對象是同一個Singleton.class,兩個線程前後執行,也就是兩個線程都會進入同步代碼塊建立一個新的對象,形成返回的uniqueInstance 並非惟一的,這樣也就不符合單例模式了。

最佳方法

從Java 1.5發行版本起,實現Singleton只須要編寫一個包含單個元素的枚舉類型:

public enum Singleton {  
    INSTANCE;  
}

使用枚舉實現單例的方法雖然尚未普遍採用,可是單元素的枚舉類型已經成爲實現Singleton的最佳方法。注意:若是Singleton必須拓展一個超類,而不是擴展Enum的時候,則不宜使用這個方法。

參考

  1. Eric Freeman;ElElisabeth Freeman.HeadFirst設計模式[M]. 北京:中國電力出版社, 2007.
  2. Joshua Bloch.Effective Java中文版(原書第3版)[M]. 北京:機械工業出版社, 2018.
  3. 漫話:如何給女友解釋什麼是單例模式?
相關文章
相關標籤/搜索