一、引言
看了李建忠老師的講的設計模式已經有一段時間了(這段時間大概有一年多了),自己還沒有寫過自己的、有關設計模式的文章。這次想寫一些關於設計模式的文章,用自己的理解和代碼來寫,算是復習一遍。寫作的過程中也會多看看其他大牛的文章,爭取讓自己的理解正確,否則把大家帶跑偏了,就是我的過錯了。今天就開始我們第一個設計模式,該模式是:【單例模式】,英文名稱:Singleton Pattern,這個模式很簡單,一個類型只需要一個實例,他是創建型的設計模式。為什么叫“創建型”設計模式呢,因為他們有分類。當然了分類的方式不一樣,分類的結果也就不一樣。
從目的來看:
-創建型(Creational)模式:負責對象創建
-結構型(Structural)模式:處理類與對象間的組合
-行為型(Behavioral)模式:類與對象交互中的職責分配
從范圍來看:
-類模式處理類與子類的靜態關系
-對象模式處理對象間的動態關系
以上就是分類的方式,我們按大多數的分類,采用“從目的來看”的分類來對設計模式進行分類,我們就開始今天的學習吧。
二、單例模式的介紹
2.1、動機(Motivate)
在軟件系統中,經常有這樣一些特殊的類,必須保證它們在系統中只存在一個實例,才能確保它們的邏輯正確性、以及良好的效率。
如何繞過常規的構造器,提供一種機制來保證一個類只有一個實例?
這應該是類設計者的責任,而不是使用者的責任
2.2、意圖(Intent)
保證一個類僅有一個實例,並提供一個該實例的全局訪問點。 --《設計模式GoF》
2.3、結構圖(Structure)
2.4、模式的組成
(1)、單件實例(Singleton):這個模式里面只有一個類型,就是Singleton類型,並且這個類只有一個實例,可以通過Instance()方法獲取該類型的實例。
2.5、單件模式的代碼實現
既然是單實例,肯定會涉及到多線程的問題,我們就一步一步的來寫代碼,我們先看看單線程Singleton模式的實現,代碼如下:
1 /// <summary> 2 /// 單例模式的實現 3 /// </summary> 4 public sealed class Singleton 5 { 6 // 定義一個靜態變量來保存類的實例 7 private static Singleton uniqueInstance; 8 9 // 定義私有構造函數,使外界不能創建該類實例 10 private Singleton() 11 { 12 } 13 14 /// <summary> 15 /// 定義公有方法提供一個全局訪問點,同時你也可以定義公有屬性來提供全局訪問點 16 /// </summary> 17 /// <returns></returns> 18 public static Singleton GetInstance() 19 { 20 // 如果類的實例不存在則創建,否則直接返回 21 if (uniqueInstance == null) 22 { 23 uniqueInstance = new Singleton(); 24 } 25 return uniqueInstance; 26 } 27 }
私有的實例構造器是屏蔽外界的調用,上面的單例模式的實現在單線程下確實是完美的,也很好的滿足了我們單線程環境的需求。
單線程單例模式的幾個要點:
(1)、Singleton模式中的實例構造器可以設置為protected以允許子類派生。
(2)、Singleton模式一般不要支持ICloneable接口,因為這可能會導致多個對象實例,與Singleton模式的初衷違背。
(3)、Singleton模式一般不要支持序列化,因為這也有可能導致多個對象實例,同樣與Singleton模式的初衷違背。
(4)、Singletom模式只考慮到了對象創建的工作,沒有考慮對象銷毀的工作。為什么這樣做呢,因為Net平台是支持垃圾回收的,所以我們一般沒有必要對其進行銷毀處理。
(5)、不能應對多線程環境:在多線程環境下,使用Singleton模式仍然有可能得到Singleton類的多個實例對象
如果放在多線程環境下,問題就出來了。因為在兩個線程同時運行GetInstance方法時,此時兩個線程判斷(uniqueInstance ==null)這個條件時都返回真,此時兩個線程就都會創建Singleton的實例,這樣就違背了我們單例模式初衷了。要想解決這個問題,只要讓GetInstance方法在同一時間只運行一個線程運行就好了,讓我們看看多線程Singleton模式的實現,代碼如下:
1 /// <summary> 2 /// 單例模式的實現 3 /// </summary> 4 public sealed class Singleton 5 { 6 // 定義一個靜態變量來保存類的實例 7 private static volatile Singleton uniqueInstance; 8 9 // 定義一個標識確保線程同步 10 private static readonly object locker = new object(); 11 12 // 定義私有構造函數,使外界不能創建該類實例 13 private Singleton() 14 { 15 } 16 17 /// <summary> 18 /// 定義公有方法提供一個全局訪問點,同時你也可以定義公有屬性來提供全局訪問點 19 /// </summary> 20 /// <returns></returns> 21 public static Singleton GetInstance() 22 { 23 // 當第一個線程運行到這里時,此時會對locker對象 "加鎖", 24 // 當第二個線程運行該方法時,首先檢測到locker對象為"加鎖"狀態,該線程就會掛起等待第一個線程解鎖 25 // lock語句運行完之后(即線程運行完之后)會對該對象"解鎖" 26 lock (locker) 27 { 28 // 如果類的實例不存在則創建,否則直接返回 29 if (uniqueInstance == null) 30 { 31 uniqueInstance = new Singleton(); 32 } 33 } 34 35 return uniqueInstance; 36 } 37 }
上面這種解決方案確實可以解決多線程的問題,但是上面代碼對於每個線程都會對線程輔助對象locker加鎖之后再判斷實例是否存在,對於這個操作完全沒有必要的,因為當第一個線程創建了該類的實例之后,后面的線程此時只需要直接判斷(uniqueInstance==null)為假,此時完全沒必要對線程輔助對象加鎖之后再去判斷,所以上面的實現方式增加了額外的開銷,損失了性能,為了改進上面實現方式的缺陷,我們只需要在lock語句前面加一句(uniqueInstance==null)的判斷就可以避免鎖所增加的額外開銷,這種實現方式我們就叫它 “雙重鎖定(Double Check)”,下面具體看看實現代碼的:
1 /// <summary> 2 /// 單例模式的實現 3 /// </summary> 4 public sealed class Singleton 5 { 6 // 定義一個靜態變量來保存類的實例 7 private static volatile Singleton uniqueInstance; 8 9 // 定義一個標識確保線程同步 10 private static readonly object locker = new object(); 11 12 // 定義私有構造函數,使外界不能創建該類實例 13 private Singleton() 14 { 15 } 16 17 /// <summary> 18 /// 定義公有方法提供一個全局訪問點,同時你也可以定義公有屬性來提供全局訪問點 19 /// </summary> 20 /// <returns></returns> 21 public static Singleton GetInstance() 22 { 23 // 當第一個線程運行到這里時,此時會對locker對象 "加鎖", 24 // 當第二個線程運行該方法時,首先檢測到locker對象為"加鎖"狀態,該線程就會掛起等待第一個線程解鎖 25 // lock語句運行完之后(即線程運行完之后)會對該對象"解鎖" 26 // 雙重鎖定只需要一句判斷就可以了 27 if (uniqueInstance == null) 28 { 29 lock (locker) 30 { 31 // 如果類的實例不存在則創建,否則直接返回 32 if (uniqueInstance == null) 33 { 34 uniqueInstance = new Singleton(); 35 } 36 } 37 } 38 return uniqueInstance; 39 } 40 }
volatile修飾:編譯器在編譯代碼的時候會對代碼的順序進行微調,用volatile修飾保證了嚴格意義的順序。一個定義為volatile的變量是說這變量可能會被意想不到地改變,這樣,編譯器就不會去假設這個變量的值了。精確地說就是,優化器在用到這個變量時必須每次都小心地重新讀取這個變量的值,而不是使用保存在寄存器里的備份。
三、C#中實現了單例模式的類
現在我們看看,如何使用C#語言的特性來實現單例的Singleton模式。
//Singleton模式的實現 public sealed class Singleton { public static readonly Singleton instance=new Singleton(); private Singleton(){} } 以上是內聯初始化(生成的同時進行初始化)的單例模式,它等同於: public sealed class Singleton { public static readonly Singleton instance; //靜態構造函數,CLR只執行一次 static Singleton() { instance=new Singleton(); } //私有構造函數,防止外界調用 private Singleton(){} }
內聯初始化其實是把靜態的字段放到靜態構造器去初始化。只要想訪問靜態字段,必定已經在之前執行了靜態構造器。這樣也能夠精確地保證使用的時候一定能拿到實例,如果不使用也不會實例化對象,也就是延時加載的功能。他同樣能夠支持多線程環境,因為只可能有一個線程執行靜態構造器,不可能有多個線程去執行靜態構造器,感覺就是程序已經自動為我們加鎖了。
它的一點弊端就是它不支持參數化的實例化方法。在.NET里靜態構造器只能聲明一個,而且必須是無參數的,私有的。因此這種方式只適用於無參數的構造器。
需要說明的是:HttpContext.Current就是一個單例,他們是通過Singleton的擴展方式實現的,他們的單例也並不是覆蓋所有領域,只是針對某些局部領域中,是單例的,不同的領域中還是會有不同的實例。
四、Singleton模式的擴展
(1)、將一個實例擴展到n個實例,例如對象池的實現。(n不是指無限個實例,而是固定的某個數)
(2)、將new構造器的調用轉移到其他類中,例如多個類協同工作環境中,某個局部環境只需要擁有某個類的一個實例。
(3)、理解和擴展Singleton模式的核心是“如何控制用戶使用new對一個類的實例構造器的任意調用”。
五、單例模式的實現要點
1、Singleton模式是限制而不是改進類的創建。
2、Singleton類中的實例構造器可以設置為Protected以允許子類派生。
3、Singleton模式一般不要支持Icloneable接口,因為這可能導致多個對象實例,與Singleton模式的初衷違背。
4、Singleton模式一般不要支持序列化,這也有可能導致多個對象實例,這也與Singleton模式的初衷違背。
5、Singleton只考慮了對象創建的管理,沒有考慮到銷毀的管理,就支持垃圾回收的平台和對象的開銷來講,我們一般沒必要對其銷毀進行特殊的管理。
6、理解和擴展Singleton模式的核心是“如何控制用戶使用new對一個類的構造器的任意調用”。
7、可以很簡單的修改一個Singleton,使它有少數幾個實例,這樣做是允許的而且是有意義的。
1】、單例模式的優點:
(1)、實例控制:Singleton 會阻止其他對象實例化其自己的 Singleton 對象的副本,從而確保所有對象都訪問唯一實例
(2)、靈活性:因為類控制了實例化過程,所以類可以更加靈活修改實例化過程
2】、單例模式的缺點:
(1)、開銷:雖然數量很少,但如果每次對象請求引用時都要檢查是否存在類的實例,將仍然需要一些開銷。可以通過使用靜態初始化解決此問題。
(2)、可能的開發混淆:使用 singleton 對象(尤其在類庫中定義的對象)時,開發人員必須記住自己不能使用 new 關鍵字實例化對象。因為可能無法訪問庫源代碼,因此應用程序開發人員可能會意外發現自己無法直接實例化此類。
(3)、對象的生存期:Singleton 不能解決刪除單個對象的問題。因為它包含對該靜態的私有字段的引用,靜態字段是不能被CLR回收內存的,該實例會和應用程序生命周期一樣長,一直存在。
3】、單例模式的使用場合:
(1)、當類只能有一個實例而且客戶可以從一個眾所周知的訪問點訪問它時。
(2)、當這個唯一實例應該是通過子類化可擴展的,並且客戶應該無需更改代碼就能使用一個擴展的實例時。
六、總結
到這里,單例模式就介紹完了,這個模式很簡單,理解起來也不是很難,只要把握住代碼的實現技巧,一般問題都不大,但是要找好使用的時機,如果使用錯誤,一些邏輯錯誤比較難排查。