1. 程式人生 > >單例模式你真的會了嗎(上篇)?

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

>單例模式相信是很多程式設計師接觸最多的了,也是面試過程中考察最頻繁的一個了,不知道你有沒有被問過這道面試題?歡迎留言討論。 今天我們來重點討論一下單例的幾個問題,及如何正確的實現一個單例,然後你再來回顧一下,你之前的回答或者使用方式是否正確。 ## 為何要使用單例 單例非常簡單,一個類只允許建立一個物件或者例項,這個類就是一個單例類。這種設計模式就叫做單例設計模式,是建立型的第一種設計模式,簡稱單例模式。 單例模式什麼時候使用呢?又或者說這種情況下為什麼要使用單例? 1. 解決資源衝突問題 比如說,我們現在的java處理程式中是使用印表機,而我們的服務端程式是多執行緒的,但是印表機只有一個,不能重複建立印表機資源啊。當然我們也可以定義普通類,在呼叫列印新增synchronized關鍵字。 2. 全域性唯一類 有時候,我們做業務設計時,有些資料在系統中只應該保留一份,這時候就應該設計為單例。 比如配置資訊類,系統的配置檔案應該只有一份,載入到記憶體之後以物件的形式存在,理所應當只有一份。 再比如說,我們設計一個抽獎系統,每點選一次生成一個抽獎序號,可以設計一個單例,內部儲存好所有的序號,每次隨機取出一個序號。如果使用普通類物件的話,那就需要通過共享記憶體共享所有抽獎序號。 ## 單例應該怎麼寫? 學習任何東西,因為大腦的容量是有限的,首先我們要理解概念,知道為什麼,來後追求怎麼做,怎麼實現,做的過程可能很複雜,比如有一二三四五步驟,但我們要化繁為簡,概括精簡。 單例需要考慮以下幾個問題: - 建構函式要是private的,這樣才能避免外部通過new建立例項嘛,不然怎麼叫單例,別人可以隨便通過new來建立啊。 - 多執行緒建立時是否有執行緒安全問題。 - 支援延遲載入嗎? - getInstance()效能高嗎? ## 單例典型實現方式 ### 餓漢式 通過這種形容方式,可以直觀的理解一下,餓漢一直擔心自己吃不飽,所以先吃了再說,也就是說例項是事先初始化好的,也就沒有辦法延遲載入了。 不支援懶載入,有人就說這種方式不好,說我都沒有使用單例,你都給我載入了,浪費啊。但是有壞處也有好處,提前把類載入進來,提前暴露問題,這樣如果類的設計有問題,在程式啟動時就會報錯,而不是等到程式執行中才暴露出來。 ```java public class SingleTon { private static final SingleTon instance = new SingleTon(); private SingleTon() {} public static SingleTon getInstance() { return instance; } public void method() {} } ``` ### 懶漢式 所謂懶漢式,那就是支援延遲載入嘍。總體思路類似,但在類內部並不是預設就把instance例項化好。 ```java 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的判斷,這樣就會重複例項化,單例就不是單例。所以為了解決多執行緒併發的問題,這裡犧牲了效能,變成了嚴格的序列制。多執行緒下效能很低。 ### 雙重檢測懶漢式 餓漢方式不支援延遲載入。 懶漢方式,多執行緒下效能低下,那怎麼修改呢,就是改進的懶漢方式,又叫雙重檢測。 具體怎麼做呢? ```java 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靜態內部類,類似餓漢方式,又做到延遲載入。 ```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的方法了。 ```java public enum SingleTon { INSTANCE; public void method() {} } ``` 但是單例模式真的就好嗎?下面我們會討論一下為什麼不推薦單例模式?如何替代,以及如何做到叢集下的分散式單例模式? 程式設計師的小夥伴們,學習之路,同行的人越多才可以走的更遠,加入公眾號[程式設計師之道],一起交流溝通,走出我們的程式設計師之道! ![掃碼加入吧!](https://img-blog.csdnimg.cn/201911011521127