- 單例模式
- 線程安全的Singleton
- 會(huì)破壞Singleton的情況
- 線程級(jí)Singleton
單例模式是幾個(gè)創(chuàng)建型模式中最獨(dú)立的一個(gè),它的主要目標(biāo)不是根據(jù)客戶程序調(diào)用生成一個(gè)新的實(shí)例,而是控制某個(gè)類型的實(shí)例數(shù)量只有一個(gè)。
GOF對(duì)單例的描述為:
Ensure a class only has one instance, and provide aglobal point of access to.
—Design Patterns : Elements of Reusable Object-Oriented Software
單例模式
單例模式的應(yīng)用場(chǎng)景不必贅述,先來(lái)一個(gè)最簡(jiǎn)單的實(shí)現(xiàn)方式:
public class Singleton
{
private Singleton() { }
private static Singleton instance;
public static Singleton Instance()
{
if (instance == null)
{
instance = new Singleton();
}
return instance;
}
}
這里采用的是Lazy方式,也可以在靜態(tài)變量被創(chuàng)建的時(shí)候直接初始化實(shí)例。
這段代碼已經(jīng)可以滿足最初Singleton模式的設(shè)計(jì)要求,在大多數(shù)情況下可以很好地工作。但在多線程環(huán)境下這種實(shí)現(xiàn)方式是存在缺陷的,當(dāng)多個(gè)線程幾乎同時(shí)調(diào)用Singleton類的Instance靜態(tài)屬性的時(shí)候,instance成員可能還沒(méi)有被實(shí)例化,因此它被創(chuàng)建了多次,而且最終Singleton類中保存的是最后創(chuàng)建的那個(gè)實(shí)例,各個(gè)線程引用的對(duì)象不同。
線程安全的Singleton
為了保證多線程環(huán)境下instance實(shí)例只有一個(gè),對(duì)代碼進(jìn)行了優(yōu)化:
public class Singleton
{
private static volatile Singleton instance;
public static Singleton Instance()
{
if (instance == null)
{
lock (typeof(Singleton))
{
if (instance == null)
{
instance = new Singleton();
}
}
}
return instance;
}
}
相比最初的實(shí)現(xiàn),改變的地方有這幾處:
- instance使用volatile關(guān)鍵字修飾,它表示字段可能被多個(gè)并發(fā)執(zhí)行的線程修改。
- 在實(shí)例化前l(fā)ock Singleton類型,避免了多個(gè)線程同時(shí)實(shí)例化的問(wèn)題。
- 第一個(gè)if加在了lock之前,是為了避免每次調(diào)用都鎖定Singleton類型帶來(lái)的效率下降。
- lock后再次判斷instance是否為空,是因?yàn)樵诟卟l(fā)場(chǎng)景下,在第一個(gè)線程鎖定并實(shí)例化期間,仍然可能會(huì)有別的線程進(jìn)入到第一層if內(nèi),這樣如果不再次判空,就會(huì)重復(fù)實(shí)例化。
會(huì)破壞Singleton的情況
有些情況會(huì)破壞Singleton的封裝,跳過(guò)“只能有一個(gè)實(shí)例”的限制,在實(shí)際應(yīng)用中要注意規(guī)避。
線程級(jí)Singleton
前面討論的是線程安全的Singleton實(shí)現(xiàn),但有時(shí)需要的是更細(xì)粒度的Singleton,比如線程級(jí)的Singleton,只要保證在一個(gè)線程內(nèi)只有一個(gè)實(shí)例即可,這就類似Asp.NET Core 自帶的IOC提供的AddScope注冊(cè)方式,可以保證一個(gè)HttpContext內(nèi)只有一個(gè)實(shí)例。
雖然Asp.NET Core提供類似的現(xiàn)成實(shí)現(xiàn),但如果在非Web環(huán)境下也需要線程級(jí)的實(shí)例控制該怎么辦呢? 結(jié)合C#提供的System.ThreadStaticAttribute可以完成
通過(guò)System.ThreadStaticAttribute可以將某個(gè)靜態(tài)變量限定為僅在本線程內(nèi)部是靜態(tài)的。
實(shí)現(xiàn)如下:
public class ThreadSingleton
{
private ThreadSingleton() { }
[ThreadStatic] //instance只在當(dāng)前線程內(nèi)為靜態(tài)
private static ThreadSingleton instance;
public static ThreadSingleton Instance()
{
if (instance == null)
{
instance = new ThreadSingleton();
}
return instance;
}
}
這里再不需要線程鎖了,因?yàn)榫€程級(jí)的單例不需要考慮線程安全。
為了驗(yàn)證實(shí)現(xiàn)的準(zhǔn)確性,首先構(gòu)造一個(gè)線程內(nèi)執(zhí)行的目標(biāo)對(duì)象:
class Work
{
public static IList<int> Log = new List<int>();
/// <summary>
/// 每個(gè)線程的執(zhí)行部分
/// </summary>
public void Procedure()
{
ThreadSingleton s1 = ThreadSingleton.Instance();
ThreadSingleton s2 = ThreadSingleton.Instance();
//證明可以正常構(gòu)造實(shí)例
Assert.IsNotNull(s1);
Assert.IsNotNull(s2);
//驗(yàn)證當(dāng)前線程執(zhí)行體內(nèi)兩次獲取的是同一個(gè)實(shí)例
Assert.AreEqual(s1.GetHashCode(), s2.GetHashCode());
//記錄當(dāng)前線程所使用對(duì)象的HashCode
Log.Add(s1.GetHashCode());
}
}
這個(gè)類會(huì)在每個(gè)線程內(nèi)部執(zhí)行,并驗(yàn)證線程內(nèi)多次獲取的Instance是同一個(gè)實(shí)例,并記錄這個(gè)實(shí)例的HashCode,以便與別的線程實(shí)例對(duì)比。
接下來(lái)開啟多個(gè)線程同時(shí)執(zhí)行Procedure()方法:
[Test]
public void ThreadSingletonTest()
{
int threadCount = 4;
Thread[] threads = new Thread[threadCount]; //創(chuàng)建4個(gè)線程
for (int i = 0; i < threadCount; i++)
{
ThreadStart work = new ThreadStart(new Work().Procedure);
threads[i] = new Thread(work);
}
//執(zhí)行線程
foreach (var thread in threads)
{
thread.Start();
}
Thread.Sleep(10000);
Assert.AreEqual(threadCount, Work.Log.Distinct().Count());
}
Work類的靜態(tài)變量Log中記錄了每個(gè)線程中實(shí)例的HashCode,這些HashCode彼此不相同,且與線程的數(shù)量一致,證明每個(gè)線程間的實(shí)例是不相同的。
參考書籍:
王翔著 《設(shè)計(jì)模式——基于C#的工程化實(shí)現(xiàn)及擴(kuò)展》
|