【轉】單例模式(singletion)

来源:http://www.cnblogs.com/mercuryli/archive/2016/03/23/5303645.html
-Advertisement-
Play Games

原地址:http://www.cnblogs.com/BoyXiao/archive/2010/05/07/1729376.html 首先來明確一個問題,那就是在某些情況下,有些對象,我們只需要一個就可以了, 比如,一臺電腦上可以連好幾個印表機,但是這個電腦上的列印程式只能有一個, 這裡就可以通 ...


單例模式(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# 中使用靜態初始化實現餓漢式單例類。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 8. 告別Lock 不是一直說Lock比較麻煩危險嗎,那就不要好了。其實有一個Lock free的方法。 首先引入一個概念——原子變數。在這種變數上的操作是原子操作(atomic operation)。原子操作就是說這個操作要麼都完成,要麼都不完成,部分完成是不行的。就像物理化學中的原子一樣,借用不 ...
  • 一、現狀說明: 就在這金三銀四的求職黃金時期,我有幸作為公司的獨立技術面試官,擁有最終決定錄用權,在倍受上級領導的充分信任下,我也向上級保證,一定要為公司找到合適的人才,就在我滿懷信心的情況下麵試了一個又一個的求職者,發現了大多數求職者共同的問題,一是:眼高手低,即工作年限雖長,但受工作內容及個人原 ...
  • 多租戶(Multi Tenancy/Tenant)是一種軟體架構,其定義是:在一臺伺服器上運行單個應用實例,它為多個租戶提供服務。 本框架使用的是共用資料庫、共用 Schema、共用數據表的數據設計架構。 進入系統管理員界面,打開租戶管理界面,如下圖所示: 下麵是租戶管理界面: 這裡可以管理租戶成員 ...
  • 本章大部分內容摘自:《領域驅動設計:軟體核心複雜性應對之道》一書中的第四章,分離領域,純屬原創。如有錯誤請指正,相互學習。 在軟體中,專門用於解決問題的那部分通常之占整個軟體的系統的很小一部分,這與其重要性遠遠不成比例。要想實現最佳的設計構思,就得去研究模型中的元素並它們視為一個系統 模式:LAYE... ...
  • PS:關於自動外呼的功能我在總體結構篇已經大概說過了,類似資料庫設計、以及相關代碼實現都不難,我就不多贅述了。這裡主要介紹當時遇到的幾個經過仔細思考過的設計思路。這幾天整理的時候,發現當時還是缺乏很多的知識,考慮也沒有很全面。這裡僅僅寫出來,供大家思考。 排隊機制的處理 板卡上的電話通道是有限的(公 ...
  • 類圖分為三層,第一層是類的名稱,如果是抽象類或介面,就用斜體表示,其中介面名稱的上部會用<<interface>>修飾;第二層是類的成員變數,通常是欄位和屬性;第三層是類的成員方法。類的成員變數和成員方法的修飾符分為+、#、-,分別表示public、protected、private。 類之間的關係 ...
  • 簡介 web的優化就是一場阻止http請求最終訪問到資料庫的戰爭。 優化的方式就是 ,在各個節點加緩存。 web請求的流程及節點 熟悉流程及節點,才能定位性能的問題。而且優化的順序一般也是按請求的流程逐一優化。這裡的流程只是做個概要,並不代表全面。 整個流程是以最快的方式讓用戶看到結果 定位的方法 ...
  • 信號的多徑傳播對環境具有依賴性,呈現出非常強的特殊性。對於每個位置而言,該位置上通道的多徑結構是惟一的,終端發射的無線電渡經過反射和折射,產生與周圍環境密切相關的特定模式的多徑信號,這樣的多徑特征可以認為是該位置的“指紋”。基站天線陣列檢測信號的幅度和相位等特性,提取多徑干擾特征參數,將該參數與預先 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...