前言
什么是單例模式?
單例模式,屬于創(chuàng)建類型的一種常用的軟件設(shè)計(jì)模式。通過(guò)單例模式的方法創(chuàng)建的類在當(dāng)前進(jìn)程中只有一個(gè)實(shí)例(根據(jù)需要,也有可能一個(gè)線程中屬于單例,如:僅線程上下文內(nèi)使用同一個(gè)實(shí)例)
上面是百度百科給出的解釋。
大家都知道,面向?qū)ο蟮乃枷刖褪俏覀兛梢园岩粋(gè)類實(shí)例很多次,每次實(shí)例出來(lái)的都是一個(gè)對(duì)象,意味著你可以創(chuàng)建很多個(gè)基于這個(gè)類的對(duì)象。
單例模式,說(shuō)白了,就是這些對(duì)象本質(zhì)都是同一個(gè),整個(gè)程序中,不管在哪里用,使用的都是同一個(gè)實(shí)例對(duì)象。
如果我們創(chuàng)建了一個(gè)China類,我們可以一直new嗎?不可以,因?yàn)槭澜缟现挥幸粋(gè)China,所以我們使用的都是同一個(gè)China對(duì)象。
Version 1 - 非線程安全
public class China
{
private China()
{
}
private static China china = null;
public static China Instance
{
get
{
if (china == null)
{
Console.WriteLine("實(shí)例化對(duì)象");
china = new China();
}
return china;
}
}
}
最簡(jiǎn)單的實(shí)現(xiàn)方式如上圖,創(chuàng)建一個(gè)私有的靜態(tài)對(duì)象和私有構(gòu)造方法,然后在CreateInstance方法里,加一個(gè)判斷,如果為Null,就重新實(shí)例化一下,否則直接返回。
這種寫法從邏輯上是沒(méi)問(wèn)題的,但是是否會(huì)出現(xiàn)這個(gè)if (china == null)判斷,同時(shí)執(zhí)行,這樣就麻煩了。
所以這種寫法在單線程的程序是沒(méi)問(wèn)題的,但是在多線程中,是可能會(huì)有問(wèn)題的。
我們做個(gè)測(cè)試,測(cè)試代碼如下:
class Program
{
static void Main(string[] args)
{
for (int i = 0; i < 10; i++)
{
new TaskFactory().StartNew(() =>
{
China china = China.Instance;
});
Thread.Sleep(10);
}
Console.ReadLine();
}
}
上面的代碼,就是創(chuàng)建10個(gè)線程,都執(zhí)行CreatInstance方法,那么最終是輸出多少次Console.WriteLine("實(shí)例化對(duì)象")呢?
我們測(cè)試發(fā)現(xiàn),這個(gè)輸出結(jié)果是不唯一的,有時(shí)候會(huì)輸出5次,有時(shí)候會(huì)輸出2次,但是一般都是超過(guò)1次,這個(gè)就說(shuō)明對(duì)象被多次實(shí)例化了,這就違背了單例模式的原則。
Version 2 - 簡(jiǎn)單的線程安全
既然出現(xiàn)問(wèn)題,那么我們就需要做一下優(yōu)化,優(yōu)化之后的代碼如下:
public class China
{
private China()
{
}
private static China china = null;
private static object objlock = new object();
public static China Instance
{
get
{
lock (objlock)
{
Console.WriteLine("執(zhí)行判斷");
if (china == null)
{
Console.WriteLine("實(shí)例化對(duì)象");
china = new China();
}
}
return china;
}
}
}
對(duì)比看下,就是加了一個(gè)同步鎖,這樣就可以避免同時(shí)執(zhí)行的情況,并且,我們?cè)趌ock里加了一個(gè)Console.WriteLine("執(zhí)行判斷"),觀察這行代碼執(zhí)行多少次。
從結(jié)果來(lái)看,實(shí)例化對(duì)象只執(zhí)行了一次,說(shuō)明對(duì)象只被創(chuàng)建過(guò)一次,滿足了我們的需求,達(dá)到了預(yù)期的效果。
Version 3 - 雙if+lock實(shí)現(xiàn)
上面那種方式已經(jīng)可以達(dá)到預(yù)期效果,但是我們注意到一個(gè)問(wèn)題,執(zhí)行判斷這行代碼被執(zhí)行了10次,這顯示不符合我們的邏輯,既然已經(jīng)實(shí)例化了,為什么每次還要執(zhí)行判斷呢?是不是多此一舉?并且每次請(qǐng)求對(duì)象,都會(huì)進(jìn)行l(wèi)ock操作,lock對(duì)性能是有一定影響的。
于是我們繼續(xù)優(yōu)化,優(yōu)化之后的代碼如下:
public class China
{
private China()
{
}
private static China china = null;
private static object objlock = new object();
public static China Instance
{
get
{
if (china == null)
{
lock (objlock)
{
Console.WriteLine("執(zhí)行判斷");
if (china == null)
{
Console.WriteLine("實(shí)例化對(duì)象");
china = new China();
}
}
}
return china;
}
}
}
我們對(duì)比代碼可以看出,就是又加了一個(gè)if (china == null),這種雙if+lock的方式,是不是可以解決我們的問(wèn)題呢?
我們執(zhí)行一次,看看結(jié)果:
我們通過(guò)結(jié)果可以看到只執(zhí)行了一次判斷,也只執(zhí)行一次實(shí)例化對(duì)象,但是我們還可以繼續(xù)優(yōu)化。
Version 4 - 靜態(tài)變量實(shí)現(xiàn)
話不多說(shuō),直接上代碼:
public class China
{
private China()
{
}
private static readonly China china = new China();
public static China Instance
{
get
{
return china;
}
}
}
利用靜態(tài)變量去實(shí)現(xiàn)單例,非常簡(jiǎn)單,但同時(shí)也是線程安全的,由CLR保證,在程序第一次使用該類之前被調(diào)用,而且只調(diào)用一次。
但是這種方式也有缺點(diǎn),就是實(shí)例化過(guò)程是在程序初始化時(shí)就執(zhí)行的,而不是在使用時(shí)才執(zhí)行,就是說(shuō),不管你用不用,都已經(jīng)實(shí)例化了。
Version 5 - 完全懶漢式實(shí)現(xiàn)
public class China
{
private China()
{
}
public static China Instance
{
get
{
return Lazy.instance;
}
}
private class Lazy
{
static Lazy()
{
}
internal static readonly China instance = new China();
}
}
這種方法與上一種方法類似,只是多加了一個(gè)類,來(lái)解決上一個(gè)版本的缺點(diǎn)。
Version 6 - 使用Lazy特性
從.NET 4開始,可以使用Lazytype來(lái)實(shí)現(xiàn)完全懶漢式,代碼也變得更簡(jiǎn)單,代碼如下:
public class China
{
private China()
{
}
private static readonly Lazy<China> lazy = new Lazy<China>(() => new China());
public static China Instance
{
get
{
return lazy.Value;
}
}
}
整體總結(jié)
可能大家看完之后,選擇困難癥又會(huì)犯了吧?
這里給大家總結(jié)一下,除了Version 1,其他幾種情況,均可以實(shí)現(xiàn)單例模式,一般情況下我們使用Vesion 2和Version 4比較多,雖然Version 2會(huì)浪費(fèi)一定的資源,但是很容易理解,實(shí)際應(yīng)用中,影響不會(huì)很大。