單例模式 - 只有一個例項

樑桂釗發表於2017-06-07

只生成一個例項的模式,我們稱之為 單例模式。

原文地址:單例模式 - 只有一個例項
部落格地址:blog.720ui.com/

程式在執行的時候,通常會有很多的例項。例如,我們建立 100 個字串的時候,會生成 100 個 String 類的例項。

但是,有的時候,我們只想要類的例項只存在一個。例如,「你猜我畫」中的畫板,在一個房間中的使用者需要共用一個畫板例項,而不是每個使用者都分配一個畫板的例項。

此外,對於資料庫連線、執行緒池、配置檔案解析載入等一些非常耗時,佔用系統資源的操作,並且還存在頻繁建立和銷燬物件,如果每次都建立一個例項,這個系統開銷是非常恐怖的,所以,我們可以始終使用一個公共的例項,以節約系統開銷。

像這樣確保只生成一個例項的模式,我們稱之為 單例模式

如何理解單例模式

單例模式的目的在於,一個類只有一個例項存在,即保證一個類在記憶體中的物件唯一性。

現在,我們來理解這個類圖。

單例模式 - 只有一個例項

靜態類成員變數

Singleton 類定義的靜態的 instance 成員變數,並將其初始化為 Singleton 類的例項。這樣,就可以保證單例類只有一個例項。

私有的構造方法

Singleton 類的構造方法是私有的,這個設計的目的在於,防止類外部呼叫該構造方法。單例模式必須要確保在任何情況下,都只能生成一個例項。為了達到這個目的,必須設定構造方法為私有的。換句話說,Singleton 類必須自己建立自己的唯一例項。

全域性訪問方法

構造方法是私有的,那麼,我們需要提供一個訪問 Singleton 類例項的全域性訪問方法。

簡要定義

保證一個類只有一個例項,並提供一個訪問它的全域性訪問方法。

單例模式的實現方式

餓漢式

顧名思義,類一載入物件就建立單例物件。

public class Singleton {

    private static Singleton instance = new Singleton();

    private Singleton(){}

    public static Singleton getInstance(){
        return instance;
    }
}複製程式碼

值得注意的是,在定義靜態變數的時候例項化 Singleton 類,因此在類載入的時候就可以建立了單例物件。

此時,我們呼叫兩次 Singleton 類的 getInstance() 方法來獲取 Singleton 的例項。我們發現 s1 和 s2 是同一個物件。

public class SingletonTest {

    @Test
    public void getInstance(){
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();

        System.out.println("例項物件1:" + s1.hashCode());
        System.out.println("例項物件2:" + s2.hashCode());
        if (s1 ==  s2) {
            System.out.println("例項相等");
        } else {
            System.out.println("例項不等");
        }
    }
}複製程式碼

懶漢式

懶漢式,即延遲載入。單例在第一次呼叫 getInstance() 方法時才例項化,在類載入時並不自動例項化,在需要的時候再進行載入例項。

public class Singleton2 {

    private Singleton2(){}

    private static Singleton2 instance = null;

    public static Singleton2 getInstance(){
        if(instance == null){
            instance = new Singleton2();
        }
        return instance;
    }
}複製程式碼

懶漢式的執行緒安全

在多執行緒中,如果使用懶漢式的方式建立單例物件,那就可能會出現建立多個例項的情況。

為了避免多個執行緒同時呼叫 getInstance() 方法,我們可以使用關鍵字 synchronized 進行執行緒鎖,以處理多個執行緒同時訪問的問題。每個類例項對應一個執行緒鎖, synchronized 修飾的方法必須獲得呼叫該方法的類例項的鎖方能執行, 否則所屬執行緒阻塞。方法一旦執行, 就獨佔該鎖,直到從該方法返回時才將鎖釋放。此後被阻塞的執行緒方能獲得該鎖, 重新進入可執行狀態。

public class Singleton3 {

    private Singleton3(){}

    private static Singleton3 instance = null;

    public static synchronized Singleton3 getInstance(){
        if(instance == null){
            instance = new Singleton3();
        }
        return instance;
    }
}複製程式碼

上面的案例,在多執行緒中很好的工作而且是執行緒安全的,但是每次呼叫 getInstance() 方法都需要進行執行緒鎖定判斷,在多執行緒高併發訪問環境中,將會導致系統效能下降。事實上,不僅效率很低,99%情況下不需要執行緒鎖定判斷。

這個時候,我們可以通過雙重校驗鎖的方式進行處理。換句話說,利用雙重校驗鎖,第一次檢查是否例項已經建立,如果還沒建立,再進行同步的方式建立單例物件。

public class Singleton4 {

    private Singleton4(){}

    private static Singleton4 instance = null;

    public static Singleton4 getInstance(){
        if(instance == null){
            synchronized(Singleton4.class){
                if(instance == null){
                    instance = new Singleton4();
                }
            }    
        }
        return instance;
    }
}複製程式碼

列舉

列舉的特點是,構造方法是 private 修飾的,並且成員物件例項都是預定義的,因此我們通過列舉來實現單例模式非常的便捷。

public enum SingletonEnum {
    INSTANCE;
    private SingletonEnum(){}
}複製程式碼

靜態內部類

類載入的時候並不會例項化 Singleton5,而是在第一次呼叫 getInstance() 載入內部類 SigletonHolder,此時才進行初始化 instance 成員變數,確保記憶體中的物件唯一性。

public class Singleton5 {
    private Singleton5() {}

    private static class SigletonHolder {
        private final static Singleton5 instance = new Singleton5();
    }

    public static Singleton5 getInstance() {
        return SigletonHolder.instance;
    }
}複製程式碼

思維發散

如何改造成單例類

假設,我們現在有一個計數類 Counter 用來統計累加次數,每次呼叫 plus() 方法會進行累加。

public class Counter {

    private long count = 0;

    public long plus(){
        return ++count;
    }
}複製程式碼

這個案例的實現方式會生成多個例項,那麼我們如何使用單例模式確保只生成一個例項物件呢?

實際上,拆解成3個步驟就可以實現我的需求:靜態類成員變數、私有的構造方法、全域性訪問方法。

public class Counter {

    private long count = 0;

    private static Counter counter = new Counter();

    private Counter(){}

    public static Counter getInstance(){
        return counter;
    }

    public synchronized long plus(){
        return ++count;
    }
}複製程式碼

多例場景

基於單例模式,我們還可以進行擴充套件改造,獲取指定個數的物件例項,節省系統資源,並解決單例物件共享過多有效能損耗的問題。

我們來做個練習,我現在有一個需求,希望實現最多隻能生成 2 個 Resource 類的例項,可以通過 getInstance() 方法進行訪問。

public class Resource {

    private int id = 0;

    private static Resource[] resource = new Resource[]{
        new Resource(1),
        new Resource(2)
    };

    private Resource(int id){
        this.id = id;
    }

    public static Resource getInstance(int id){
        return resource[id];
    }
}複製程式碼

單例模式 vs 靜態方法

如果認為單例模式是非靜態方法。而靜態方法和非靜態方法,最大的區別在於是否常駐記憶體,實際上是不對的。它們都是在第一次載入後就常駐記憶體,所以方法本身在記憶體裡,沒有什麼區別,所以也就不存在靜態方法常駐記憶體,非靜態方法只有使用的時候才分配記憶體的結論。

因此,我們要從場景的層面來剖析這個問題。如果一個方法和他所在類的例項物件無關,僅僅提供全域性訪問的方法,這種情況考慮使用靜態類,例如 java.lang.Math。而使用單例模式更加符合物件導向思想,可以通過繼承和多型擴充套件基類。此外,上面的案子中,單例模式還可以進行延伸,對例項的建立有更自由的控制。

單例模式與資料庫連線

資料庫連線並不是單例的,如果一個系統中只有一個資料庫連線例項,那麼全部資料訪問都使用這個連線例項,那麼這個設計肯定導致效能缺陷。事實上,我們通過單例模式確保資料庫連線池只有一個例項存在,通過這個唯一的連線池例項分配 connection 物件。

總結

單例模式的目的在於,一個類只有一個例項存在,即保證一個類在記憶體中的物件唯一性。

如果採用餓漢式,在類被載入時就例項化,因此無須考慮多執行緒安全問題,並且物件一開始就得以建立,效能方面要優於懶漢式。

如果採用懶漢式,採用延遲載入,在第一次呼叫 getInstance() 方法時才例項化。好處在於無須一直佔用系統資源,在需要的時候再進行載入例項。但是,要特別注意多執行緒安全問題,我們需要考慮使用雙重校驗鎖的方案進行優化。

實際上,我們應該採用餓漢式還是採用懶漢式,取決於我們希望空間換取時間,還是時間換取空間的抉擇問題。

此外,列舉和靜態內部類也是非常不錯的實現方式。

參考文章

(書)「圖解設計模式」(結城浩)

原始碼

相關示例完整程式碼: design-pattern-action

(完)

更多精彩文章,盡在「服務端思維」微信公眾號!

單例模式 - 只有一個例項

相關文章