單例模式你真的會了嗎(上篇)?
- 2020 年 3 月 8 日
- 筆記
單例模式相信是很多程式設計師接觸最多的了,也是面試過程中考察最頻繁的一個了,不知道你有沒有被問過這道面試題?歡迎留言討論。
今天我們來重點討論一下單例的幾個問題,及如何正確的實現一個單例,然後你再來回顧一下,你之前的回答或者使用方式是否正確。
為何要使用單例
單例非常簡單,一個類只允許創建一個對象或者實例,這個類就是一個單例類。這種設計模式就叫做單例設計模式,是創建型的第一種設計模式,簡稱單例模式。
單例模式什麼時候使用呢?又或者說這種情況下為什麼要使用單例?
- 解決資源衝突問題
比如說,我們現在的java處理程式中是使用印表機,而我們的服務端程式是多執行緒的,但是印表機只有一個,不能重複創建印表機資源啊。當然我們也可以定義普通類,在調用列印添加synchronized關鍵字。 -
全局唯一類
有時候,我們做業務設計時,有些數據在系統中只應該保留一份,這時候就應該設計為單例。
比如配置資訊類,系統的配置文件應該只有一份,載入到記憶體之後以對象的形式存在,理所應當只有一份。
再比如說,我們設計一個抽獎系統,每點擊一次生成一個抽獎序號,可以設計一個單例,內部存儲好所有的序號,每次隨機取出一個序號。如果使用普通類對象的話,那就需要通過共享記憶體共享所有抽獎序號。單例應該怎麼寫?
學習任何東西,因為大腦的容量是有限的,首先我們要理解概念,知道為什麼,來後追求怎麼做,怎麼實現,做的過程可能很複雜,比如有一二三四五步驟,但我們要化繁為簡,概括精簡。
單例需要考慮以下幾個問題:
- 構造函數要是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() {} }
但是單例模式真的就好嗎?下面我們會討論一下為什麼不推薦單例模式?如何替代,以及如何做到集群下的分散式單例模式?
程式設計師的小夥伴們,學習之路,同行的人越多才可以走的更遠,加入公眾號[程式設計師之道],一起交流溝通,走出我們的程式設計師之道!