【轉】單例模式(singletion)


單例模式(Singleton)

原地址:http://www.cnblogs.com/BoyXiao/archive/2010/05/07/1729376.html

首先來明確一個問題,那就是在某些情況下,有些對象,我們只需要一個就可以了,

比如,一台計算機上可以連好幾個打印機,但是這個計算機上的打印程序只能有一個,

這里就可以通過單例模式來避免兩個打印作業同時輸出到打印機中,

即在整個的打印過程中我只有一個打印程序的實例。

簡單說來,單例模式(也叫單件模式)的作用就是保證在整個應用程序的生命周期中,

任何一個時刻,單例類的實例都只存在一個(當然也可以不存在)。

    

              

下面來看單例模式的結構圖(圖太簡單了)

image

從上面的類圖中可以看出,在單例類中有一個構造函數 Singleton ,

但是這個構造函數卻是私有的(前面是“ - ”符號),

然后在里面還公開了一個 GetInstance()方法,

通過上面的類圖不難看出單例模式的特點,從而也可以給出單例模式的定義

單例模式保證一個類僅有一個實例,同時這個類還必須提供一個訪問該類的全局訪問點。

先來將 Singleton 寫出來再說

         

        

Singleton 類

namespace Singleton
{
    public class Singleton
    {
        //定義一個私有的靜態全局變量來保存該類的唯一實例
        private static Singleton singleton;

        /// <summary>
        /// 構造函數必須是私有的
        /// 這樣在外部便無法使用 new 來創建該類的實例
        /// </summary>

        private Singleton()
        {
        }

       /// <summary>
        /// 定義一個全局訪問點
        /// 設置為靜態方法
        /// 則在類的外部便無需實例化就可以調用該方法
        /// </summary>
        /// <returns></returns>

        public static Singleton GetInstance()
        {
        
   //這里可以保證只實例化一次
            //即在第一次調用時實例化
            //以后調用便不會再實例化

            if (singleton == null)
            {
                singleton = new Singleton();
            }
            return singleton;
        }
    }
}

客戶端代碼

using System;

namespace SingletonTest
{
    class Program
    {
        static void Main(string[] args)
        {
            Singleton.Singleton singletonOne =
                Singleton.Singleton.GetInstance();
            Singleton.Singleton singletonTwo =
                Singleton.Singleton.GetInstance();

            if (singletonOne.Equals(singletonTwo))
            {
                Console.WriteLine("singletonOne 和 singletonTwo 代表的是同一個實例");
            }
            else
            {
                Console.WriteLine("singletonOne 和 singletonTwo 代表的是不同一個實例");
            }

            Console.ReadKey();
        }
    }
}

運行結果為

image

從上面的結果可以看出來,盡管我兩次訪問了 GetInstance(),但是我訪問的只是同一個實例,

換句話來說,上面的代碼中,由於構造函數被設置為 private 了,

所以您無法再在 Singleton 類的外部使用 new 來實例化一個實例,您只能通過訪問 GetInstance()來訪問 Singleton 類,

GetInstance()通過如下方式保證該 Singleton 只存在一個實例:

首先這個 Singleton 類會在在第一次調用 GetInstance()時創建一個實例,並將這個實例的引用封裝在自身類中,

然后以后調用 GetInstance()時就會判斷這個 Singleton 是否存在一個實例了,如果存在,則不會再創建實例。

而是調用以前生成的類的實例,這樣下來,整個應用程序中便就只存在一個實例了。

從這里再來總結單例模式的特點:

首先,單例模式使類在程序生命周期的任何時刻都只有一個實例,

然后,單例的構造函數是私有的,外部程序如果想要訪問這個單例類的話,

必須通過 GetInstance()來請求(注意是請求)得到這個單例類的實例。

                     

                             

有的時候,總是容易把全局變量和單例模式給弄混了,下面就剖析一下全局變量和單例模式相比的缺點

首先,全局變量呢就是對一個對象的靜態引用,全局變量確實可以提供單例模式實現的全局訪問這個功能,

但是,它並不能保證您的應用程序中只有一個實例,同時,在編碼規范中,也明確指出,

應該要少用全局變量,因為過多的使用全局變量,會造成代碼難讀,

還有就是全局變量並不能實現繼承(雖然單例模式在繼承上也不能很好的處理,但是還是可以實現繼承的)

而單例模式的話,其在類中保存了它的唯一實例,這個類,它可以保證只能創建一個實例,

同時,它還提供了一個訪問該唯一實例的全局訪問點。

               

            

上面呢,差不多就將單例模式的核心給介紹完了,

或許,您會覺得單例模式就這么個東西啊,不就是保證只有一個實例嘛,也太簡單了,

如果您真這么想的話,那您就錯了,因為要保證在整個應用程序生命周期中保證只有一個實例不是那么容易的,

下面就來看一種情況(這里先假設我的應用程序是多線程應用程序),同時還是以前面的 Demo 來做為說明,

如果在一開始調用 GetInstance()時,是由兩個線程同時調用的(這種情況是很常見的),注意是同時,

(或者是一個線程進入 if 判斷語句后但還沒有實例化 Singleton 時,第二個線程到達,此時 singleton 還是為 null)

這樣的話,兩個線程均會進入 GetInstance(),而后由於是第一次調用 GetInstance(),

所以存儲在 Singleton 中的靜態變量 singleton 為 null ,這樣的話,就會讓兩個線程均通過 if 語句的條件判斷,

然后調用 new Singleton()了,

        public static Singleton GetInstance()
        { 
            if (singleton == null)
            {
                singleton = new Singleton();
            }
            return singleton;
        }

這樣的話,問題就出來了,因為有兩個線程,所以會創建兩個實例,

很顯然,這便違法了單例模式的初衷了,

那么如何解決上面出現的這個問題(即多線程下使用單例模式時有可能會創建多個實例這一現象)呢?

其實,這個是很好解決的,

您可以這樣思考這個問題:

由於上面出現的問題中涉及到多個線程同時訪問這個 GetInstance(),

那么您可以先將一個線程鎖定,然后等這個線程完成以后,再讓其他的線程訪問 GetInstance()中的 if 段語句,

比如,有兩個線程同時到達

如果 singleton != null 的話,那么上面提到的問題是不會存在的,因為已經存在這個實例了,這樣的話,

所有的線程都無法進入 if 語句塊,

也就是所有的線程都無法調用語句 new Singleton()了,

這樣還是可以保證應用程序生命周期中的實例只存在一個,

但是如果此時的 singleton == null 的話,

那么意味着這兩個線程都是可以進入這個 if 語句塊的,

那么就有可能出現上面出現的單例模式中有多個實例的問題,

此時,我可以讓一個線程先進入 if 語句塊,然后我在外面對這個 if 語句塊加鎖,

對第二個線程呢,由於 if 語句進行了加鎖處理,所以這個進程就無法進入 if 語句塊而處於阻塞狀態,

當進入了 if 語句塊的線程完成 new  Singleton()后,這個線程便會退出 if 語句塊,

此時,第二個線程就從阻塞狀態中恢復,即就可以訪問 if 語句塊了,但是由於前面的那個線程已近創建了 Singleton 的實例,

所以 singleton != null ,此時,第二個線程便無法通過 if 語句的判斷條件了,

即無法進入 if 語句塊了,這樣便保證了整個生命周期中只存在一個實例,

也就是只有第一個線程創建了 Singleton 實例,第二個線程則無法創建實例。

下面就來重新改進前面 Demo 中的 Singleton 類,使其在多線程的環境下也可以實現單例模式的功能。

namespace Singleton
{
    public class Singleton
    {
        //定義一個私有的靜態全局變量來保存該類的唯一實例
        private static Singleton singleton;

        //定義一個只讀靜態對象
        //且這個對象是在程序運行時創建的

        private static readonly object syncObject = new object();

        /// <summary>
        /// 構造函數必須是私有的
        /// 這樣在外部便無法使用 new 來創建該類的實例
        /// </summary>

       private Singleton()
        {

        }

       /// <summary>
        /// 定義一個全局訪問點
        /// 設置為靜態方法
        /// 則在類的外部便無需實例化就可以調用該方法
        /// </summary>
        /// <returns></returns>

        public static Singleton GetInstance()
        {
        
   //這里可以保證只實例化一次
            //即在第一次調用時實例化
            //以后調用便不會再實例化
 

            //第一重 singleton == null
            if (singleton == null)
            {
               
lock (syncObject)
                {

                            //第二重 singleton == null

                    if (singleton == null)
                    {
                        singleton = new Singleton();
                    }

                }
            }
            return singleton;
        }
    }
}

上面的就是改進后的代碼,可以看到在類中有定義了一個靜態的只讀對象  syncObject,

這里需要說明的是,為何還要創建一個 syncObject 靜態只讀對象呢?

由於提供給 lock 關鍵字的參數必須為基於引用類型的對象,該對象用來定義鎖的范圍,

所以這個引用類型的對象總不能為 null 吧,而一開始的時候,singleton 為 null ,所以是無法實現加鎖的,

所以必須要再創建一個對象即 syncObject 來定義加鎖的范圍。

還有要解釋一下的就是在 GetInstance()中,我為什么要在 if 語句中使用兩次判斷 singleton == null ,

這里涉及到一個名詞 Double-Check Locking ,也就是雙重檢查鎖定,

為何要使用雙重檢查鎖定呢?

考慮這樣一種情況,就是有兩個線程同時到達,即同時調用 GetInstance(),

此時由於 singleton == null ,所以很明顯,兩個線程都可以通過第一重的 singleton == null ,

進入第一重 if 語句后,由於存在鎖機制,所以會有一個線程進入 lock 語句並進入第二重 singleton == null ,

而另外的一個線程則會在 lock 語句的外面等待。

而當第一個線程執行完 new  Singleton()語句后,便會退出鎖定區域,此時,第二個線程便可以進入 lock 語句塊,

此時,如果沒有第二重 singleton == null 的話,那么第二個線程還是可以調用 new  Singleton()語句,

這樣第二個線程也會創建一個 Singleton 實例,這樣也還是違背了單例模式的初衷的,

所以這里必須要使用雙重檢查鎖定。

細心的朋友一定會發現,如果我去掉第一重 singleton == null ,程序還是可以在多線程下完好的運行的,

考慮在沒有第一重 singleton == null 的情況下,

當有兩個線程同時到達,此時,由於 lock 機制的存在,第一個線程會進入 lock 語句塊,並且可以順利執行 new Singleton(),

當第一個線程退出 lock 語句塊時, singleton 這個靜態變量已不為 null 了,所以當第二個線程進入 lock 時,

還是會被第二重 singleton == null 擋在外面,而無法執行 new Singleton(),

所以在沒有第一重 singleton == null 的情況下,也是可以實現單例模式的?那么為什么需要第一重 singleton == null 呢?

這里就涉及一個性能問題了,因為對於單例模式的話,new Singleton()只需要執行一次就 OK 了,

而如果沒有第一重 singleton == null 的話,每一次有線程進入 GetInstance()時,均會執行鎖定操作來實現線程同步,

這是非常耗費性能的,而如果我加上第一重 singleton == null 的話,

那么就只有在第一次,也就是 singleton ==null 成立時的情況下執行一次鎖定以實現線程同步,

而以后的話,便只要直接返回 Singleton 實例就 OK 了而根本無需再進入 lock 語句塊了,這樣就可以解決由線程同步帶來的性能問題了。

好,關於多線程下單例模式的實現的介紹就到這里了,但是,關於單例模式的介紹還沒完。

                    

                 

                   

下面將要介紹的是懶漢式單例和餓漢式單例

懶漢式單例

何為懶漢式單例呢,可以這樣理解,單例模式呢,其在整個應用程序的生命周期中只存在一個實例,

懶漢式呢,就是這個單例類的這個唯一實例是在第一次使用 GetInstance()時實例化的,

如果您不調用 GetInstance()的話,這個實例是不會存在的,即為 null

形象點說呢,就是你不去動它的話,它自己是不會實例化的,所以可以稱之為懶漢。

其實呢,我前面在介紹單例模式的這幾個 Demo 中都是使用的懶漢式單例,

看下面的 GetInstance()方法就明白了:

        public static Singleton GetInstance()
        {

            if (singleton == null)
            {
               
lock (syncObject)
                {

                    if (singleton == null)
                    {
                        singleton = new Singleton();
                    }

                }
            }
            return singleton;
        }

從上面的這個 GetInstance()中可以看出這個單例類的唯一實例是在第一次調用 GetInstance()時實例化的,

所以此為懶漢式單例。

               

            

餓漢式單例

上面介紹了餓漢式單例,到這里來理解懶漢式單例的話,就容易多了,懶漢式單例由於人懶,

所以其自己是不會主動實例化單例類的唯一實例的,而餓漢式的話,則剛好相反,

其由於肚子餓了,所以到處找東西吃,人也變得主動了很多,所以根本就不需要別人來催他實例化單例類的為一實例,

其自己就會主動實例化單例類的這個唯一類。

在 C# 中,可以用特殊的方式實現餓漢式單例,即使用靜態初始化來完成餓漢式單例模式

下面就來看一看餓漢式單例類

namespace Singleton
{
   
public sealed class Singleton
    {
        private static readonly Singleton singleton = new Singleton();

        private Singleton()
        {
        }

        public static Singleton GetInstance()
        {
            return singleton;
        }
    }
}

要先在這里提一下的是使用靜態初始化的話,無需顯示地編寫線程安全代碼,

C# 與 CLR 會自動解決前面提到的懶漢式單例類時出現的多線程同步問題。

上面的餓漢式單例類中可以看到,當整個類被加載的時候,就會自行初始化 singleton 這個靜態只讀變量。

而非在第一次調用 GetInstance()時再來實例化單例類的唯一實例,所以這就是一種餓漢式的單例類。

               

             

                

好,到這里,就真正的把單例模式介紹完了,在此呢再總結一下單例類需要注意的幾點:

一、單例模式是用來實現在整個程序中只有一個實例的。

二、單例類的構造函數必須為私有,同時單例類必須提供一個全局訪問點。

三、單例模式在多線程下的同步問題和性能問題的解決。

四、懶漢式和餓漢式單例類。

五、C# 中使用靜態初始化實現餓漢式單例類。


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com