單例模式你真的會了嗎(上篇)?

單例模式相信是很多程序員接觸最多的了,也是面試過程中考察最頻繁的一個了,不知道你有沒有被問過這道面試題?歡迎留言討論。

今天我們來重點討論一下單例的幾個問題,及如何正確的實現一個單例,然後你再來回顧一下,你之前的回答或者使用方式是否正確。

爲何要使用單例

單例非常簡單,一個類只允許創建一個對象或者實例,這個類就是一個單例類。這種設計模式就叫做單例設計模式,是創建型的第一種設計模式,簡稱單例模式。

單例模式什麼時候使用呢?又或者說這種情況下爲什麼要使用單例?

  1. 解決資源衝突問題
    比如說,我們現在的java處理程序中是使用打印機,而我們的服務端程序是多線程的,但是打印機只有一個,不能重複創建打印機資源啊。當然我們也可以定義普通類,在調用打印添加synchronized關鍵字。
  2. 全局唯一類
    有時候,我們做業務設計時,有些數據在系統中只應該保留一份,這時候就應該設計爲單例。
    比如配置信息類,系統的配置文件應該只有一份,加載到內存之後以對象的形式存在,理所應當只有一份。
    再比如說,我們設計一個抽獎系統,每點擊一次生成一個抽獎序號,可以設計一個單例,內部存儲好所有的序號,每次隨機取出一個序號。如果使用普通類對象的話,那就需要通過共享內存共享所有抽獎序號。

單例應該怎麼寫?

學習任何東西,因爲大腦的容量是有限的,首先我們要理解概念,知道爲什麼,來後追求怎麼做,怎麼實現,做的過程可能很複雜,比如有一二三四五步驟,但我們要化繁爲簡,概括精簡。

單例需要考慮以下幾個問題:

  • 構造函數要是private的,這樣才能避免外部通過new創建實例嘛,不然怎麼叫單例,別人可以隨便通過new來創建啊。
  • 多線程創建時是否有線程安全問題。
  • 支持延遲加載嗎?
  • getInstance()性能高嗎?

單例典型實現方式

餓漢式

通過這種形容方式,可以直觀的理解一下,餓漢一直擔心自己喫不飽,所以先吃了再說,也就是說實例是事先初始化好的,也就沒有辦法延遲加載了。
不支持懶加載,有人就說這種方式不好,說我都沒有使用單例,你都給我加載了,浪費啊。但是有壞處也有好處,提前把類加載進來,提前暴露問題,這樣如果類的設計有問題,在程序啓動時就會報錯,而不是等到程序運行中才暴露出來。

public class SingleTon {
    private static final SingleTon instance = new SingleTon();
    
    private SingleTon() {}
    
    public static SingleTon getInstance() {
        return instance;
    }
    
    public void method() {}
}

懶漢式

所謂懶漢式,那就是支持延遲加載嘍。總體思路類似,但在類內部並不是默認就把instance實例化好。

public class SingleTon {
    private static SingleTon instance;

    private SingleTon() {}

    public static synchronized SingleTon getInstance() {
        if (instance == null) {
            instance = new SingleTon();
        }
        return instance;
    }

    public void method() {}
}

爲什麼要加synchronized呢?如果是多線程同時調用getInstance(),會有併發問題啊,多個線程可能同時拿到instance == null的判斷,這樣就會重複實例化,單例就不是單例。所以爲了解決多線程併發的問題,這裏犧牲了性能,變成了嚴格的串行制。多線程下性能很低。

雙重檢測懶漢式

餓漢方式不支持延遲加載。
懶漢方式,多線程下性能低下,那怎麼修改呢,就是改進的懶漢方式,又叫雙重檢測。
具體怎麼做呢?

public class SingleTon {
    private static volatile SingleTon instance;

    private SingleTon() {}

    public static SingleTon getInstance() {
        if (instance == null) {
            synchronized (SingleTon.class) {
                if (instance == null) {
                    instance = new SingleTon();
                }
            }
        }
        return instance;
    }

    public void method() {}
}

這個類裏的volatile十分關鍵,如果沒有volatile關鍵字修飾instance變量,如果線程1執行到instance = new SingleTon();的時候,線程2此時判斷instance已經不等於null了,會直接返回instance,但此時instance並未初始化完畢,爲什麼這麼說呢?因爲對象的初始化分爲三步:

  • 分配內存
  • 內存初始化
  • 對象指向新分配的內存地址

既然是分爲三步,那就不是原子操作,而且可能會發生指令重排,也就是說可能先執行第三步,這時候其他線程判斷instance也就不是null了。加上volatile關鍵字,可以禁止機器指令重排,就不會有這個問題了。

靜態內部類

這種方式,避免雙重檢測,利用java靜態內部類,類似餓漢方式,又做到延遲加載。

public class SingleTon {

    private SingleTon() {}
    
    private static class SingleTonHolder {
        private static final SingleTon instance = new SingleTon();
    }

    public static SingleTon getInstance() {
        return SingleTonHolder.instance;
    }

    public void method() {}
}

是不是覺得很簡潔?推薦大家使用這種方式,類SingleTon加載時,並不會加載SingleTonHolder類,只要調用getInstance方法時,SingleTonHolder纔會被加載,並創建instance,這些都是由JVM來保證的。

枚舉方式

還有一種更簡單的,但是理解起來可能有點費解,枚舉的構造函數默認就是私有的。java的枚舉類型本身就保證了線程安全性和實例唯一性。
只需要簡單幾行,就可以使用枚舉單例INSTANCE的方法了。

public enum SingleTon {
    INSTANCE;
    
    public void method() {}
}

但是單例模式真的就好嗎?下面我們會討論一下爲什麼不推薦單例模式?如何替代,以及如何做到集羣下的分佈式單例模式?

程序員的小夥伴們,學習之路,同行的人越多才可以走的更遠,加入公衆號[程序員之道],一起交流溝通,走出我們的程序員之道!
掃碼加入吧!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章