單例模式 - 只有一個實例

只生成一個實例的模式,咱們稱之爲 單例模式。javascript

原文地址:單例模式 - 只有一個實例
博客地址:blog.720ui.com/java

程序在運行的時候,一般會有不少的實例。例如,咱們建立 100 個字符串的時候,會生成 100 個 String 類的實例。git

可是,有的時候,咱們只想要類的實例只存在一個。例如,「你猜我畫」中的畫板,在一個房間中的用戶須要共用一個畫板實例,而不是每一個用戶都分配一個畫板的實例。github

此外,對於數據庫鏈接、線程池、配置文件解析加載等一些很是耗時,佔用系統資源的操做,而且還存在頻繁建立和銷燬對象,若是每次都建立一個實例,這個系統開銷是很是恐怖的,因此,咱們能夠始終使用一個公共的實例,以節約系統開銷。數據庫

像這樣確保只生成一個實例的模式,咱們稱之爲 單例模式設計模式

如何理解單例模式

單例模式的目的在於,一個類只有一個實例存在,即保證一個類在內存中的對象惟一性。 安全

如今,咱們來理解這個類圖。微信

靜態類成員變量

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

(完)

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

相關文章
相關標籤/搜索