【原】從頭學習設計模式(一)——單例模式

一、引入
單例模式作為23種設計模式中的最基礎的一種模式,在平時開發中應用也非常普遍。到底哪些類應該設計成單例的呢,我們來舉個最通俗的例子。在一個父容器中單擊某個菜單項打開一個子窗口,如果不使用單例又沒有作菜單項的可用控制的話,每次單擊菜單項都會打開一個新窗口。這不僅會浪費內存資源,在程序邏輯上也是不可以接受的。
二、最簡單的單例
下面來看一個最簡單的單例模式的構造形式
public class Program { private static void Main(string[] args) { GetInstance(); Console.ReadLine(); } private void GetInstance() { Singleton objectSingleton = Singleton.getInstance(); } } public class Singleton { private static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); Console.WriteLine("Create a singleton instance!"); } return instance; } }
這里通過幾個關鍵的地方以保證返回唯一的實例:
- 私有的構造器,防止客戶端用new關鍵字去創建實例對象
- 靜態的instance對象,保證全局唯一性
- 對外的公共的靜態實例方法,從類級別直接可調用此方法
- 通過判斷null值,決定是創建新實例還是直接返回。
三、并發下的單例
不過,以上標準的單例構造模式在多并發的情況下就有可能失效,請參考下面的這種情況。
private static void Main(string[] args) { //開啟10個線程來模擬多并發的情況 for (int i = 0; i < 10; i++) { Thread thread = new Thread(new ThreadStart(GetInstance)); thread.Start(); } Console.ReadLine(); } private static void GetInstance() { Singleton objectSingleton = Singleton.getInstance(); } } public class Singleton { private static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { //讓線程睡一會,來模擬有多個進程都已經在調用getInstance方法 Thread.Sleep(10); if (instance == null) { instance = new Singleton(); Console.WriteLine("Create a singleton instance!"); } return instance; } }
以上的程序中開啟了10個線程,來模擬多個用戶同時訪問getInstance方法。在getInstance方法里,開始先讓線程睡一會,以模擬多個線程都已經進入了該方法。從運行的結果看上,基本上不能保證實例化唯一的實例了。

產生這種問題的原因就是,當一個線程進入getInstance 方法,通過了 instance==null 的條件,還沒有來的及執行 instance = new Singleton() 時,另一個線程也通過了條件判斷,這時就會 new Singleton() 多次了。
解決的辦法就是加鎖,只讓一個線程進來,參考以下代碼:
public class Program { private static void Main(string[] args) { //開啟100個線程來模擬多并發的情況 for (int i = 0; i < 100; i++) { Thread thread = new Thread(new ThreadStart(GetInstance)); thread.Start(); } Console.ReadLine(); } private static void GetInstance() { Singleton objectSingleton = Singleton.getInstance(); } } public class Singleton { private static Singleton instance = null; private static readonly object objForLock=new object(); private Singleton() { } public static Singleton getInstance() { //讓線程睡一會,來模擬有多個進程都已經在調用getInstance方法 Thread.Sleep(10); //加一道鎖只讓一個線程進來 lock (objForLock) { if (instance == null) { instance = new Singleton(); Console.WriteLine("Create a singleton instance!"); } } return instance; } }
這樣差不多可以達到我們的要求,只有一個實例對象存在可以保證了,但是這樣會帶來一點性能問題。如果每個線程進入getInstance方法后,都要lock一下以進行線程同步的話,每個線程到這里都要等待解鎖后才能進入,但是如果 instance已經不是null了,那就應該直接返回而不是等待解鎖,所以這里需要引入“雙重鎖”的單例構造方法,代碼如下:
private static void Main(string[] args) { //開啟100個線程來模擬多并發的情況 for (int i = 0; i < 100; i++) { Thread thread = new Thread(new ThreadStart(GetInstance)); thread.Start(); } Console.ReadLine(); } private static void GetInstance() { Singleton objectSingleton = Singleton.getInstance(); } } public class Singleton { private static Singleton instance = null; private static readonly object objForLock=new object(); private Singleton() { } public static Singleton getInstance() { //讓線程睡一會,來模擬有多個進程都已經在調用getInstance方法 Thread.Sleep(10); //先判斷是不是已經實例化過了,避免進入鎖等待狀態 if(instance ==null) { //加一道鎖只讓一個線程進來 lock (objForLock) { if (instance == null) { instance = new Singleton(); Console.WriteLine("Create a singleton instance!"); } } } return instance; } }
四、“懶漢式”單例與“餓漢式”單例的比較
所謂的“懶漢式”與“餓漢式”,就是實例對象的初始化時間不同:
1. 在需要實例化的時候才進行實例化,就是懶漢式。我們上面的這些例子都是懶漢式的。懶漢式有好處當然是節省資源啦,需要的時候才去構造,平時不用占用內存。不過,這會帶來上面提到的在多并發下需要加鎖限制訪問的問題。
2.類加載時就直接進行實例化,就是餓漢式。例如下面的代碼:
private static Singleton instance = new Singleton(); private Singleton() { } public static Singleton getInstance() { return instance; }
餓漢式單例是當類第一次被加載時就會將instance進行實例化,這樣作的好處是不會帶來多并發下的線程安全問題了,也不用考慮鎖了。但反過來帶來的就是性能問題了,因為不管你需不需要將這個實例對象創建出來,它都已經被創建出來常駐內存了。所以如果是一個工廠模式、緩存了很多實例、那么就得考慮效率問題,因為這個類一加載則把所有實例不管用不用一塊創建。
這里只是對兩種構造模式作了一個簡單的對比,實際項目中要應用哪種模式還是要看具體需求而定。


浙公網安備 33010602011771號