設計模式筆記-單例模式
定義
確保某一個類只有一個例項,並且提供一個全域性訪問點。
單例模式具備典型的3個特點
- 只有一個例項。 2、自我例項化。 3、提供全域性訪問點。
為什麼會有單例模式
單例模式的使用自然是當我們的系統中某個物件只需要一個例項的情況,例如:作業系統中只能有一個工作管理員,操作檔案時,同一時間內只允許一個例項對其操作等,既然現實生活中有這樣的應用場景,自然在軟體設計領域必須有這樣的解決方案了(因為軟體設計也是現實生活中的抽象),所以也就有了單例模式了。
類只有一個例項。
在C#中通過私有建構函式來保證類外部不能對類進行例項化
提供一個全域性的訪問點
建立一個返回該類物件的靜態方法
使用場景
● 要求生成唯一序列號的環境;
● 在整個專案中需要一個共享訪問點或共享資料,例如一個Web頁面上的計數器,可以不用把每次重新整理都記錄到資料庫中,使用單例模式保持計數器的值,並確保是執行緒安全的;
● 建立一個物件需要消耗的資源過多,如要訪問IO和資料庫等資源;
● 需要定義大量的靜態常量和靜態方法(如工具類)的環境,可以採用單例模式(當然,也可以直接宣告為static的方式)。
C#
單例模式的實現程式碼
一. 非執行緒安全
//Bad code! Do not use! public sealed class Singleton { private static Singleton instance = null; private Singleton() { } public static Singleton instance { get { if (instance == null) { instance = new Singleton(); } return instance; } } }
這種方法不是執行緒安全的,會存在兩個執行緒同時執行if (instance == null)
並且建立兩個不同的instance,後建立的會替換掉新建立的,導致之前拿到的reference為空。
二. 簡單的執行緒安全實現
public sealed class Singleton { private static Singleton instance = null; private static readonly object padlock = new object(); Singleton() { } public static Singleton Instance { get { lock (padlock) { if (instance == null) { instance = new Singleton(); } return instance; } } } }
相比較於實現一,這個版本加上了一個對instance的鎖,在呼叫instance之前要先對padlock上鎖,這樣就避免了實現一中的執行緒衝突,該實現自始至終只會建立一個instance了。但是,由於每次呼叫Instance都會使用到鎖,而呼叫鎖的開銷較大,這個實現會有一定的效能損失。
注意這裡我們使用的是新建一個private的object例項padlock來實現鎖操作,而不是直接對Singleton進行上鎖。直接對型別上鎖會出現潛在的風險,因為這個型別是public的,所以理論上它會在任何code裡呼叫,直接對它上鎖會導致效能問題,甚至會出現死鎖情況。
Note: C#中,同一個執行緒是可以對一個object進行多次上鎖的,但是不同執行緒之間如果同時上鎖,就可能會出現執行緒等待,或者嚴重的會出現死鎖情況。因此,我們在使用lock時,儘量選擇類中的私有變數上鎖,這樣可以避免上述情況發生。
三. 雙重驗證的執行緒安全實現
public sealed calss Singleton
{
private static Singleton instance = null;
private static readonly object padlock = new object();
Singleton()
{
}
public static Singleton Instance
{
get
{
if (instance == null)
{
lock (padlock)
{
if (instance == null)
{
instance = new Singleton();
}
}
}
return instance;
}
}
}
在保證執行緒安全的同時,這個實現還避免了每次呼叫Instance都進行lock操作,這會節約一定的時間。
但是,這種實現也有它的缺點:
1. 無法在Java中工作。(具體原因可以見原文,這邊沒怎麼理解)
2. 程式設計師在自己實現時很容易出錯。如果對這個模式的程式碼進行自己的修改,要倍加小心,因為double check的邏輯較為複雜,很容易出現思考不周而出錯的情況。
四. 不用鎖的執行緒安全實現
public sealed class Singleton
{
//在Singleton第一次被呼叫時會執行instance的初始化
private static readonly Singleton instance = new Singleton();
//Explicit static consturctor to tell C# compiler
//not to mark type as beforefieldinit
static Singleton()
{
}
private Singleton()
{
}
public static Singleton Instance
{
get
{
return instance;
}
}
}
這個實現很簡單,並沒有用到鎖,但是它仍然是執行緒安全的。這裡使用了一個static,readonly的Singleton例項,它會在Singleton第一次被呼叫的時候新建一個instance,這裡新建時候的執行緒安全保障是由.NET直接控制的,我們可以認為它是一個原子操作,並且在一個AppDomaing中它只會被建立一次。
這種實現也有一些缺點:
1. instance被建立的時機不明,任何對Singleton的呼叫都會提前建立instance
2. static建構函式的迴圈呼叫。如有A,B兩個類,A的靜態建構函式中呼叫了B,而B的靜態建構函式中又呼叫了A,這兩個就會形成一個迴圈呼叫,嚴重的會導致程式崩潰。
3. 我們需要手動新增Singleton的靜態建構函式來確保Singleton型別不會被自動加上beforefieldinit這個Attribute,以此來確保instance會在第一次呼叫Singleton時才被建立。
4. readonly的屬性無法在執行時改變,如果我們需要在程式執行時dispose這個instance再重新建立一個新的instance,這種實現方法就無法滿足。
五. 完全延遲載入實現(fully lazy instantiation)
public sealed class Singleton
{
private Singleton()
{
}
public static Singleton Instance
{
get
{
return Nested.instance;
}
}
private class Nested
{
// Explicit static constructor to tell C# compiler
// not to mark type as beforefieldinit
static Nested()
{
}
internal static readonly Singleton instance = new Singleton();
}
}
實現五是實現四的包裝。它確保了instance只會在Instance的get方法裡面呼叫,且只會在第一次呼叫前初始化。它是實現四的確保延遲載入的版本。
六 使用.NET4的Lazy<T>型別
public sealed class Singleton
{
private static readonly Lazy<Singleton> lazy = new Lazy<Singleton>(() => new Singleton());
public static Singleton Instance
{
get
{
return lazy.Value;
}
}
private Singleton()
{
}
}
.NET4或以上的版本支援Lazy<T>來實現延遲載入,它用最簡潔的程式碼保證了單例的執行緒安全和延遲載入特性。
.NET FrameWork的SR實現了單例模式
總結:
雙重鎖定的作用:
第一個判斷null是為了儘量減少進入鎖的執行緒數;
第二個判斷null是為了防止進入鎖只有的多個執行緒重複建立例項;
優點:
一、例項控制
單例模式會阻止其他物件例項化其自己的單例物件的副本,從而確保所有物件都訪問唯一例項。
二、靈活性
因為類控制了例項化過程,所以類可以靈活更改例項化過程。
缺點:
一、開銷
雖然數量很少,但如果每次物件請求引用時都要檢查是否存在類的例項,將仍然需要一些開銷。可以通過使用靜態初始化解決此問題。
二、可能的開發混淆
使用單例物件(尤其在類庫中定義的物件)時,開發人員必須記住自己不能使用new關鍵字例項化物件。因為可能無法訪問庫原始碼,因此應用程式開發人員可能會意外發現自己無法直接例項化此類。
三、物件生存期
不能解決刪除單個物件的問題。在提供記憶體管理的語言中(例如基於.NET Framework的語言),只有單例類能夠導致例項被取消分配,因為它包含對該例項的私有引用。在某些語言中(如 C++),其他類可以刪除物件例項,但這樣會導致單例類中出現懸浮引用。
單例適用性
使用Singleton模式有一個必要條件:在一個系統要求一個類只有一個例項時才應當使用單例模式。反之,如果一個類可以有幾個例項共存,就不要使用單例模式。
不要使用單例模式存取全域性變數。這違背了單例模式的用意,最好放到對應類的靜態成員中。
不要將資料庫連線做成單例,因為一個系統可能會與資料庫有多個連線,並且在有連線池的情況下,應當儘可能及時釋放連線。Singleton模式由於使用靜態成員儲存類例項,所以可能會造成資源無法及時釋放,帶來問題。
擴充套件學習:
當欄位被標記為beforefieldinit型別時,該欄位初始化可以發生在任何時候任何欄位被引用之前。
當類包含靜態欄位,而且沒有定義靜態的建構函式時,該類會被標記為beforefieldinit
現在也許有人會問:“被標記為beforefieldinit和沒有標記的有什麼區別呢”?OK現在讓我們通過下面的具體例子看一下它們的區別吧!
class Test
{
public static string x = EchoAndReturn("In type initializer");
static Test()
{
}
public static string EchoAndReturn(string s)
{
Console.WriteLine(s);
return s;
}
}
class Driver
{
public static void Main()
{
Console.WriteLine("Starting Main");
Test.EchoAndReturn("Echo!");
Console.WriteLine("After echo");
Console.ReadLine();
}
}
我相信大家都可以得到答案,如果在呼叫EchoAndReturn()方法之前,需要完成靜態成員的初始化,所以最終的輸出結果如下:
圖5輸出結果
接著我們在Main()方法中新增string y = Test.x,如下:
public static void Main()
{
Console.WriteLine("Starting Main");
Test.EchoAndReturn("Echo!");
Console.WriteLine("After echo");
string y = Test.x;
//Use the value just to avoid compiler cleverness
if (y != null)
{
Console.WriteLine("After field access");
}
Console.ReadKey();
}
圖6 輸出結果
通過上面的輸出結果,大家可以發現靜態欄位的初始化跑到了靜態方法呼叫之前
參考文獻:
https://www.cnblogs.com/xmfdsh/p/4036927.html
http://www.cnblogs.com/zhili/p/SingletonPatterm.html
https://www.jianshu.com/p/3ae1bd656c1f
https://www.jb51.net/article/100783.htm