天天看點

寂然解讀設計模式 - 單例模式(上)

I walk very slowly, but I never walk backwards            

設計模式 - 單例模式(上)

​ 寂然

大家好~,我是寂然,本節課呢,我們正式進入設計模式的學習,第一個要聊的就是我們最常見的,同時也是面試高頻的單例模式,那老規矩,我們直接啟程吧

五重境界

在此之前,我們閑聊兩句,因為有的小夥伴問到一個問題,怎麼才算是學會或者說掌握設計模式了呢?那我們一起來聊聊這個話題,會有很多人說其實這個不好去定義,但是如果自己平時寫代碼,無意識,習慣成自然的用到了設計模式,遇到實際問題,很快就能對比選擇,使用合适的設計模式,低耦合,合理的解決掉實際問題,我覺得就是很好的掌握了,網上有人分享了設計模式的五重境界,感覺很貼切,這裡也拿來和大家分享下

第 1 層:剛開始學程式設計不久,聽說過什麼是設計模式 ,但不了解

第 2 層:有很長時間的程式設計經驗了,自己寫了很多代碼,其中用到了設計模式,但是自己卻不知道

第 3 層:學習過了設計模式,發現自己已經在使用了,并且發現了一些新的模式挺好用的

第 4 層:閱讀了很多别人寫的源碼和架構,其中看到了設計模式,能夠領會設計模式的精妙和帶來的好處

第 5 層:代碼寫着寫着,自己都沒有意識到使用了設計模式,并且熟練的寫了出來

問題答疑

同時,在開始之間還要和大家強調一句話,因為很多小夥伴都有說或者腦海中有這樣一個疑問:這樣就可以啊,為什麼非要這麼複雜呢,大家要注意,設計模式不僅僅是站在實作功能的角度上,而是立足于整個軟體系統的設計,是以大家要學會在學習的過程中,總結和對比思考,可以達到事半功倍的效果

官方定義

所謂類的單例設計模式,就是采取一定的方法保證在整個的軟體系統中,對某個類隻能存在一個對象執行個體并且該類隻提供一個取得其對象執行個體的方法(靜态方法)

舉個最常見的例子,Spring 中的 bean 預設都是單例模式,每個bean定義隻生成一個對象執行個體,每次getBean請求獲得的都是此執行個體

單例模式八種方式

那接下來我們來聊聊單例模式的八種實作方式,如下所示

餓漢式(靜态常量)

餓漢式(靜态代碼塊)

懶漢式(線程不安全)

懶漢式(線程安全,同步方法)

懶漢式(線程安全,同步代碼塊)

雙重檢查

靜态内部類

枚舉方式

餓漢式(靜态常量)

餓漢式之靜态常量的寫法如下:

一,構造器私有化(防止外部通過new + 構造器的方式建立對象執行個體)

二,類的内部建立對象(建立final,static的執行個體對象)

三,對外暴露一個公共的靜态方法(通過該方法,傳回該類唯一的對象執行個體)

// 餓漢式(靜态變量)
class Singleton{
    //一 構造器私有化
    private Singleton(){

    }
    //類的内部建立對象
    private final static Singleton singleton = new Singleton();

    //對外提供公共的,靜态的方法
    public static Singleton getInstance(){

            return singleton;
    }
}

public class SingletonDemo {
    public static void main(String[] args) {

        //測試
        Singleton instance = Singleton.getInstance();
        Singleton instance1 = Singleton.getInstance();
        System.out.println(instance == instance1);
        //結果為 true,證明是同一個執行個體對象
    }
}           
寫法分析

優勢

這種寫法比較簡單,就是在類裝載的時候就完成了執行個體化,避免了線程同步問題

這種方式基于 classloder 機制避免了多線程的同步問題,不過,instance 在類裝載時就執行個體化,在單例模式中大 多數都是調用 getInstance 方法,但是導緻類裝載的原因有很多種,是以不能确定有其他的方式(或者其他的靜态方法)導緻類裝載,這時候初始化 instance 就沒有達到 lazy loading 的效果

劣勢

在類裝載的時候就完成執行個體化,沒有達到 Lazy Loading 的效果,如果從始至終從未使用過這個執行個體,則會造成記憶體的浪費

餓漢式(靜态代碼塊)

餓漢式(靜态代碼塊)的方式,是在靜态代碼塊中建立單例對象,示例代碼如下圖所示

//餓漢式 靜态代碼塊
class Singleton{

    private Singleton(){

    }

    private final static Singleton singleton;

    static {
        singleton = new Singleton();
    }

    public static Singleton getInstance(){

        return singleton;
    }
}

public class Singletondemo02 {

    public static void main(String[] args) {

        Singleton instance = Singleton.getInstance();
        Singleton instance1 = Singleton.getInstance();
        System.out.println(instance == instance1);

    }
}           

這種方式和上面的其實類似,隻不過将類執行個體化的過程放在了靜态代碼塊中,也是在類裝載的時候,就執行靜态代碼塊中的代碼,初始化類的執行個體,優劣勢和第一種寫法一緻,寫法可用,但可能會造成記憶體的浪費

餓漢式是不存線上程安全問題的

懶漢式(線程不安全)

懶漢式寫法與餓漢式寫法的不同在于,并不是一上來就要建立對象,而是當調用對應的getInstance()方法時,才會進行建立,示例代碼如下圖所示

// 懶漢式(線程不安全)
class Singleton{

    //構造器私有化
    private Singleton(){ 

    }

    //類的内部建立對象
    private static Singleton singleton;

    //提供一個靜态的公有方法,當使用到該方法時,才會去建立 instance
    public static Singleton getInstance(){

        fiif (singleton == null){
            singleton = new Singleton();
        }
        return singleton;
    }
}

public class SingletonDemo {
    public static void main(String[] args) {
        Singleton instance = Singleton.getInstance();
        Singleton instance1 = Singleton.getInstance();
        System.out.println(instance == instance1);

    }
}           

起到了 lady loading 的效果,不會造成記憶體浪費,執行個體使用到的時候,調用getInstance()方法才會建立

隻能在單線程下使用,多線程會出現線程不安全的問題

試想,如果在多線程下,一個線程進入了 if (singleton == null){ ...} 判斷語句塊,還沒來得及往下執行,另一個線程進行判斷,此時執行個體還未來得及建立,也通過了這個判斷語句,這時便會産生多個執行個體,出現了線程不安全問題

是以,在實際開發中,不建議使用這種方式

懶漢式(同步方法)

針對上面線程不安全的問題,我們在懶漢式的代碼中,加入同步處理,第一種就是同步方法,相關代碼如下

//懶漢式 同步方法
class Singleton{

    //構造器私有化
    private Singleton(){

    }

    //類的内部建立對象
    public static Singleton singleton;

    //對外提供公共的靜态方法,加入同步處理
    public static synchronized Singleton getInstance(){

        if (singleton == null){
            singleton = new Singleton();  
        }
        return singleton;
    }
}

public class SingletonDemo02 {
    public static void main(String[] args) {

        Singleton instance = Singleton.getInstance();
        Singleton instance1 = Singleton.getInstance();
        System.out.println(instance == instance1);
    }
}           

首先,這種寫法,由于加入了同步處理,解決了線程不安全的問題

但是我們說,這樣效率很低,每個線程在想獲得類的執行個體的時候,執行 getInstance() 方法都要進行同步,而其實這個方法隻執行一次執行個體化代碼就夠了,後面的想獲得該類的執行個體,直接return就好了,方法進行同步,效率很低

是以,在實際開發中,也不推薦使用這種方式

懶漢式(同步代碼塊)

那既然方法進行同步,效率很低,我們可不可以使用同步代碼塊的方式呢?大家看如下的示例代碼

class Singleton{

    //構造器私有化
    private Singleton(){ }

    //類的内部建立對象
    public static Singleton singleton;

    //對外提供公共的靜态方法
    public static Singleton getInstance(){

        if (singleton == null){

            synchronized (Singleton.class){
                singleton = new Singleton();  
            }

        }
        return singleton;
    }
}           

這種寫法就是,本意是想對第四種實作方式的改進,在建立執行個體對象的時候,進行同步,那大家思考一下,這樣可行嘛?可以保證線程安全嘛

但其實,如果在多線程下,一個線程進入了 if (singleton == null){ ...} 判斷語句塊,還沒來得及往下執行,另一個線程進行判斷,此時執行個體還未來得及建立,也通過了這個判斷語句,便會産生多個執行個體,是以在裡面建立執行個體對象的時候,進行同步沒有實際意義,在實際開發中,不能使用這種方式

下節預告

OK,由于篇幅的限制,本節内容就先到這裡,下一節,我們接着來聊單例模式的後三種寫法,包括雙重檢查機制,靜态内部類,枚舉,同時會帶大家看下JDK源碼中單例模式的應用,以及對單例模式的注意事項進行總結,最後,希望大家在學習的過程中,能夠感覺到設計模式的有趣之處,高效而愉快的學習,那我們下期見~