用心理解設計模式——單例模式 (Singleton Pattern)
前置文章: 用心理解設計模式——設計模式的原則
設計模式相關程式碼已統一放至 我的 Github
一、定義
建立型模式之一。
Ensure a class has only one instance, and provide a global point of access to it.
(確保某一個類只有一個例項,而且自行例項化並吸納過整個系統提供這個例項)
二、要點
將類的非靜態建構函式私有化。只在類內部例項一次。
三、評價
只允許建立單個例項,滿足一些管理需求,節省記憶體,加快物件訪問速度等。
四、實現方式
1.餓漢式
顧名思義:比較飢渴、比較急,不管用不用,類載入時就先建立例項。
public sealed class Singleton
{
static private readonly Singleton instance = new Singleton();
static public Singleton GetInstance()
{
return instance;
}
private Singleton() { }
}
顧名思義:比較懶,等到用的時候才建立例項。
public sealed class Singleton
{
static private Singleton instance;
static public Singleton GetInstance()
{
if (instance == null) { instance = new Singleton(); }
return instance;
}
private Singleton() { }
}
兩種基本方式比較:
執行緒安全?:
餓漢式,是執行緒安全的,因為,類只會載入一次,例項必然只會建立一次。
懶漢式,是執行緒不安全的,因為,多執行緒下,如果不同執行緒同時進入 if判斷,就會生成多個例項物件。
延遲例項化?:
餓漢式,不管是否使用都會建立例項,如果最終沒有使用,造成記憶體浪費。
懶漢式,實際使用時才建立例項,節省記憶體。
3.靜態初始化式 (餓漢模式在Lazy Loading上的優化(不徹底)。.Net中首選的方式)
public sealed class Singleton
{
static private readonly Singleton instance;
static public Singleton GetInstance()
{
return instance;
}
//靜態構造
static Singleton()
{
instance = new Singleton();
}
private Singleton() { }
}
要點理解: C#靜態建構函式
特點:它是執行緒安全的, 並且將例項化從類載入時延遲到了類首次使用時。
4.雙重檢查鎖模式 (飽漢模式在多執行緒上的優化)
public sealed class Singleton
{
private static Singleton instance;
private static readonly object theLock = new object ();
private Singleton () { }
public static Singleton GetInstance ()
{
// 第一重檢查, 避免 “例項已經存在時仍然執行lock獲取鎖, 影響效能” 的問題。
if (instance == null)
{
//讓執行緒排隊執行被lock包圍的程式碼段
lock (theLock)
{
//第二重檢查, 僅讓隊首的執行緒進入if建立例項。
if (instance == null)
{
instance = new Singleton ();
}
}
}
return instance;
}
}
要點理解: C# 的 lock 關鍵字 。
特點:它是執行緒安全的,並且將例項化延遲到了單例使用時。
5.靜態內部類式
public sealed class Singleton
{
//此靜態內部類,只有在第一次使用時才會載入
static private class Holder
{
static public readonly Singleton instance = new Singleton();
}
static public Singleton GetInstance()
{
return Holder.instance;
}
private Singleton() { }
}
要點理解:內部類在第一次使用時才被載入,其直接在定義時初始化的靜態成員也在此時進行初始化。
特點:它是執行緒安全的,並且將例項化延遲到了單例使用時。
6.Lazy<T>式
//注意:僅.NET4 以上可用
public sealed class Singleton
{
static private readonly Lazy<Singleton> lazy = new Lazy<Singleton>(() => new Singleton());
public static Singleton GetInstance
{
return lazy.Value;
}
private Singleton() { }
}
要點理解:如何執行物件的延遲初始化
特點:它是執行緒安全的,並且將例項化延遲到了單例使用時。適合較大單例物件。
五、繼承和複用問題
建構函式被宣告為私有的類, 不能被繼承。
因為子類在例項化時需要先呼叫父類的建構函式,而父類建構函式因為是私有的所以不可訪問。
在C#中嘗試,會報錯如下圖。
單例類的建構函式被宣告為私有,所以單例類不能被繼承。
通常,單例類會直接宣告為sealed,來明確表示不能被繼承。
--------------------------------------------------------------------------------------------------------
在實際專案中,是有可能出現大量單例類的,如果每個都要實現一遍單例似乎比較麻煩,
利用泛型可以勉強解決這個問題。 為什麼說勉強,請看下面程式碼。
( 這裡以 靜態初始化式來展示,其他方式類似)。
namespace Singleton.Generic
{
//泛型單例抽象類
public abstract class Singleton<T> where T : class, new()
{
static private readonly T instance;
static public T GetInstance()
{
return instance;
}
//靜態構造
static Singleton()
{
// T要能被例項化,必須在where中新增 new() 約束,同時T必須提供一個公有的無參建構函式
instance = new T();
}
//為了能被子類繼承,只能放棄父類中對單例的建構函式進行私有化這個要點
//private Singleton() { }
}
//通過繼承實現實際的單例類
public class A : Singleton<A>
{
//為了能夠被例項化,只能放棄子類中對單例的建構函式進行私有化這個要點
//private A() { }
}
public class Client
{
private void Main()
{
//期望這樣訪問
A instatnce = A.GetInstance();
//實際因為沒私有化構造方法,可以在外部執行多次例項化
A a = new A();
A aa = new A();
A aaa = new A();
}
}
}
這樣做的缺點非常明顯,
為了能繼承和複用,父子類中都放棄了 “對建構函式進行私有化”這個單例模式要點,實際可以在外部執行多次例項化。
之後,我又嘗試了使用反射 (T)Activator.CreateInstance<T>() 的方式建立例項,這時如果對T的建構函式進行私有化,雖然在編譯階段不會報錯,但會在執行時報錯:MissingMethodException: Method not found: 'Default constructor not found...ctor() of A'. 可見子類要想在父類中被例項化,必須得提供至少有一個公有的建構函式。(Java中似乎可以通過反射在執行時對建構函式setAccessible,改為public進行構造)
結論:如果想要對單例模式進行繼承複用,只能公開建構函式,但是這樣違背單例模式的例項限制,不安全,不推薦使用。
六、靜態類和單例模式
兩者用起來比較相似,讓人常常很糾結到底應該選哪個。
從以下幾個方面作對比:
1.最直觀的,看類是否有成員變數,如果一個類中只是提供一些方法,沒有成員變數。那實在沒必要使用單例類,直接用靜態類即可。
否則,
2.從執行效率,靜態類執行效率更高。
3.從成員大小,如果類的職責比較龐大,並且只是在需要的時候才可能用到。就應該考慮是否需要懶載入優化記憶體,靜態類最多推遲到在靜態構造方法中初始化靜態成員變數,而單例可以推遲到實際使用時。
4.從程式設計思想,單例類面向物件,可以繼承類(注意不能被繼承,上邊說了),可以有動態多型,可以實現介面。靜態類面向過程
5.從使用簡便性,靜態類直接 類名.方法() ;單例模式要用 類名.GetInstance().方法()。