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


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

單例有什么用處?

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

Singleton通常用來代表那些本質上唯一的系統組件,比如窗口管理器或者文件系統。

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

經典的單例模式實現

對於一個簡單的單例模式,可以這樣實現:

  1. 定義一個私有的靜態變量uniqueInstance;

  2. 定義私有的構造方法。這樣別處的代碼無法通過調用該類的構造函數來實例化該類的對象,只能通過該類提供的靜態方法來得到該類的唯一實例;

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

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. 漫話:如何給女朋友解釋什么是單例模式?


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM