這篇博客的標題用了一個疑問句,源於我們公司的代碼評審,深刻的討論了單例模式的使用場景及其與靜態方法來說有何不同,這次討論確實讓我真正的理解了單例模式的使用,雖然說理解還一定全面,但必須作為一個認知的提升。告訴了我自己,對於編程,不懂的太多,原理性的東西還需要持續的學習。 進入正文,我們來討論一下,什 ...
這篇博客的標題用了一個疑問句,源於我們公司的代碼評審,深刻的討論了單例模式的使用場景及其與靜態方法來說有何不同,這次討論確實讓我真正的理解了單例模式的使用,雖然說理解還一定全面,但必須作為一個認知的提升。告訴了我自己,對於編程,不懂的太多,原理性的東西還需要持續的學習。
進入正文,我們來討論一下,什麼是單例模式,何時使用單例模式?
單例模式是經典設計模式的一種,熟悉設計模式或者說讀過設計模式相關書籍的同事都知道,這應該算是設計模式中最簡單、最容易理解、使用最廣泛的一種。單例模式主要是用來實現一個類的實例全局唯一,使用double check的形式來定義。
1 public class SingleInstance 2 { 3 private static readonly object _lock = new object(); 4 private static SingleInstance _instance = null; 5 6 /// <summary> 7 /// 私有構造函數 8 /// </summary> 9 private SingleInstance() { } 10 11 /// <summary> 12 /// 單一實例 13 /// </summary> 14 /// <returns></returns> 15 public static SingleInstance GetInstance() 16 { 17 if (_instance == null) 18 { 19 lock (_lock) 20 { 21 if (_instance == null) 22 { 23 _instance = new SingleInstance(); 24 } 25 } 26 } 27 return _instance; 28 } 29 30 31 public void Show() 32 { 33 Console.WriteLine("輸出。。。郭志奇"); 34 } 35 36 public void Speak() 37 { 38 Console.WriteLine("說話。。。郭志奇"); 39 } 40 }
單例模式使用了私有構造函數來保證外部無法實例化、使用double check來保證實例被唯一創建。這是一個基本的單例模式寫法,我一般會在其中寫一些方法來進行調用,主要是為了避免每次調用都需要new的麻煩。但其中存在一些問題,如果採用靜態方法來寫:
1 public static void Show() 2 { 3 Console.WriteLine("輸出。。。郭志奇"); 4 } 5 6 public static void Speak() 7 { 8 Console.WriteLine("說話。。。郭志奇"); 9 }
比較這兩種調用,其實使用方式是一致的,但單例模式會在程式運行中一直存在,不會被銷毀,因為單例模式中使用到了靜態變數,靜態變數的使用會導致實例不會被銷毀。但這也不應該是單例模式的缺點。
但我為什麼會說我們真的懂單例模式?
回到開頭,我們說單例模式,為什麼我們需要單例模式,絕對不是因為方便調用,因為靜態方法更方便。那到底為什麼使用單例模式呢?其實經過我們的討論,單例模式的使用場景是一些全局不可變參數,可以放到單例中,比如從配置獲取值,然後緩存到單例中,這才是我們應當使用單例的場景,千萬別像我,為了使用方便而無節制的使用單例。
使用單例,方便調用,但會造成什麼問題呢?
要回答這個問題,我們首先回憶一下GC的垃圾回收機制,垃圾回收分為三代,如果類中包含靜態成員,垃圾回收機制是不會回收的,也就意味著如果我們無節制的使用單例,會造成程式運行過程中出現大量的實例不會被銷毀,會無意識的造成記憶體使用增高。 如果採用懶載入的方式,在單例未被調用的時候,不會實例化,如果採用餓漢載入的話,那麼在程式初始化的時候,就會被初始化,無疑會加重程式的初始化成本,增加啟動時間。
如果我們僅僅是為了方便調用,可以使用靜態方法。
上面我們說了懶載入方式,我們來代碼說明一下餓漢模式的載入方式:
1 public class SingleInstance 2 { 3 private static readonly object _lock = new object(); 4 private static SingleInstance _instance = new SingleInstance(); 5 6 /// <summary> 7 /// 私有構造函數 8 /// </summary> 9 private SingleInstance() { } 10 11 /// <summary> 12 /// 單一實例 13 /// </summary> 14 /// <returns></returns> 15 public static SingleInstance GetInstance() 16 { 17 return _instance; 18 } 19 20 21 public void Show() 22 { 23 Console.WriteLine("輸出。。。郭志奇"); 24 } 25 26 public void Speak() 27 { 28 Console.WriteLine("說話。。。郭志奇"); 29 } 30 }
餓漢模式的載入就是靜態成員在定義的時候即初始化。
總結:
1、我們應該選擇合適的時機使用單例模式,不要無節制的使用,應該明白何時才應該使用單例模式。
2、儘量避免靜態成員的使用,因為靜態成員所在的實例,不會被GC回收。
3、優先選擇靜態方法調用而不是單例模式調用。
4、如果必須使用單例模式,儘量採用懶載入,而不是餓漢載入的方式,減少程式啟動成本。
引申:
1、我們使用了lock(object)來鎖定一個變數,達到加鎖的目的,避免多個線程同時對實例執行初始化。那麼如果我們lock(string 字元串類型)是否可以呢?答案是否定。
2、System.String和string有什麼不同呢?
歡迎有不同見解的同事可以回覆討論,知識總是在討論中得到升華。