1. 程式人生 > >設計模式的征途—1.單例(Singleton)模式

設計模式的征途—1.單例(Singleton)模式

  單例模式屬於建立型模式的一種,建立型模式是一類最常用的設計模式,在軟體開發中應用非常廣泛。建立型模式將物件的建立和使用分離,在使用物件時無需關心物件的建立細節,從而降低系統的耦合度,讓設計方案更易於修改和擴充套件。每一個建立型模式都在檢視回答3個問題:3W -> 建立什麼(What)、由誰建立(Who)和何時建立(When)。

  本篇是建立型模式的第一篇,也是最簡單的一個設計模式,雖然簡單,但是其使用頻率確是很高的。

單例模式(Singleton) 學習難度:★☆☆☆☆ 使用頻率:★★★★☆

一、單例模式的動機

  相信大家都使用過Windows工作管理員,我們可以做一個嘗試:在Windows工作列的右鍵選單上多次點選“啟動工作管理員”,看能否開啟多個工作管理員視窗。正常情況下,無論我們啟動多少次,Windows系統始終只能彈出一個工作管理員視窗。也就是說,在一個Windows系統中,工作管理員存在唯一性。

  在實際開發中,我們經常也會遇到類似的情況,為了節約系統資源,有時候需要確保系統中某個類只有唯一一個例項,當這個唯一例項建立成功之後,無法再建立一個同類型的其他物件,所有的操作都只能基於這個唯一例項。為了確保物件的唯一性,可以通過建立單例模式來實現,這也就是單例模式的動機所在。

二、單例模式概述

2.1 要點

單例(Singleton)模式:確保某一個類只有一個例項,而且自行例項化並向整個系統提供這個例項,這個類稱為單例類,它提供全域性訪問的方法。單例模式是一種物件建立模式。

  單例模式有3個要點:

    • 某個類只能有一個例項
    • 它必須自行建立這個例項
    • 它必須自行向整個系統提供這個例項  

2.2 結構圖

  從上圖中可以看出,單例模式結構圖中只包含了一個單例的角色。

  Singleton(單例):

    • 在單例類的內部實現只生成一個例項,同時它提供一個靜態的GetInstance()方法,讓客戶可以訪問它的唯一例項;
    • 為了防止在外部對單例類例項化,它的建構函式被設為private;
    • 在單例類的內部定義了一個Singleton型別的靜態物件,作為提供外部共享的唯一例項。

三、負載均衡器的設計

3.1 軟體需求

  假設M公司成都分公司的IT開發部門承接了一個伺服器負載均衡器(Load Balance)軟體的開發,該軟體執行在一臺負載均衡伺服器上面,可以將併發訪問和資料流量分發到伺服器叢集中的多臺裝置上進行併發處理,提高系統的整體處理能力,縮短響應時間。由於叢集中的伺服器需要動態增減,且客戶端請求需要統一分發,因此需要確保負載均衡器的唯一性,即只能有一個負載均衡器例項來管理伺服器和分發請求,否則會帶來伺服器狀態的不一致以及請求的分配衝突等問題。

  如何確保負載均衡器的唯一性成為了這個軟體成功地關鍵。

3.2 擼起袖子加油幹

  成都分公司的開發人員通過分析和權衡,決定使用單例模式來設計這個負載均衡器,於是擼起袖子畫了一個結構圖如下:

  在上圖所示的UML圖中,將LoadBalancer類設計為了單例類,其中包含了一個儲存伺服器資訊的集合serverList,每次在serverList中隨機選擇一臺伺服器來響應客戶端的請求,其實現程式碼如下:

    /// <summary>
    /// 假裝自己是一個負載均衡器
    /// </summary>
    public class LoadBalancer
    {
        // 私有靜態變數,儲存唯一例項
        private static LoadBalancer instance = null;
        // 伺服器集合
        private IList<CustomServer> serverList = null;

        // 私有建構函式
        private LoadBalancer()
        {
            serverList = new List<CustomServer>();
        }

        // 公共靜態成員方法,返回唯一例項
        public static LoadBalancer GetLoadBalancer()
        {
            if (instance == null)
            {
                instance = new LoadBalancer();
            }

            return instance;
        }

        // 新增一臺Server
        public void AddServer(CustomServer server)
        {
            serverList.Add(server);
        }

        // 移除一臺Server
        public void RemoveServer(string serverName)
        {
            foreach (var server in serverList)
            {
                if (server.Name.Equals(serverName))
                {
                    serverList.Remove(server);
                    break;
                }
            }
        }

        // 獲得一臺Server - 使用隨機數獲取
        private Random rand = new Random();
        public CustomServer GetServer()
        {
            int index = rand.Next(serverList.Count);

            return serverList[index];
        }
    }

    /// <summary>
    /// 假裝自己是一臺伺服器
    /// </summary>
    public class CustomServer
    {
        public string Name { get; set; }
        public int Size { get; set; }
    }

  現在我們在客戶端程式碼中新增一些測試程式碼,看看結果:

    public class Program
    {
        public static void Main(string[] args)
        {
            LoadBalancer balancer, balancer2, balancer3;
            balancer = LoadBalancer.GetLoadBalancer();
            balancer2 = LoadBalancer.GetLoadBalancer();
            balancer3 = LoadBalancer.GetLoadBalancer();

            // 判斷負載均衡器是否相同
            if (balancer == balancer2 && balancer == balancer3 && balancer2 == balancer3)
            {
                Console.WriteLine("^_^ : 伺服器負載均衡器是唯一的!");
            }

            // 增加伺服器
            balancer.AddServer(new CustomServer() { Name = "Server 1" });
            balancer.AddServer(new CustomServer() { Name = "Server 2" });
            balancer.AddServer(new CustomServer() { Name = "Server 3" });
            balancer.AddServer(new CustomServer() { Name = "Server 4" });

            // 模擬客戶端請求的分發
            for (int i = 0; i < 10; i++)
            {
                CustomServer server = balancer.GetServer();
                Console.WriteLine("該請求已分配至 : " + server.Name);
            }

            Console.ReadKey();
        }
    }

  執行客戶端程式碼,檢視執行結果:

  從執行結果中我們可以看出,雖然我們建立3個LoadBalancer物件,但是它們實際上是同一個物件。因此,通過使用單例模式可以確保LoadBalancer物件的唯一性。

3.3 餓漢式與懶漢式單例

  在進行測試時,成都分公司的測試人員發現負載均衡器在啟動過程中使用者再次啟動負載均衡器時,系統無任何異常,但當客戶端提交請求時出現請求分發失敗,通過仔細分析發現原來系統中還是會存在多個負載均衡器的物件,從而導致分發時目標伺服器不一致,從而產生衝突。

  開發部人員對實現程式碼進行再一次分析,當第一次呼叫GetLoadBalancer()方法建立並啟動負載均衡器時,instance物件為null,因此係統將會例項化其物件,在此過程中,由於要對LoadBalancer進行大量初始化工作,需要一段時間來建立LoadBalancer物件。而在此時,如果再一次呼叫GetLoadBalancer()方法(通常發生在多執行緒環境中),由於instance尚未建立成功,仍為null值,於是會再次例項化LoadBalancer物件,最終導致建立了多個instance物件,這也就違背了單例模式的初衷,導致系統發生執行錯誤。

  So,如何解決這個問題?也就有了下面的餓漢式與懶漢式的解決方案。

 (1)餓漢式單例 

  懶漢式單例實現起來最為簡單,在C#中,我們可以利用靜態建構函式來實現。於是我們可以改寫以上的程式碼塊:

    public class LoadBalancer
    {
        // 私有靜態變數,儲存唯一例項
        private static readonly LoadBalancer instance = new LoadBalancer();

        ......

        // 公共靜態成員方法,返回唯一例項
        public static LoadBalancer GetLoadBalancer()
        {
            return instance;
        }
    }

C#的語法中有一個函式能夠確保只調用一次,那就是靜態建構函式。由於C#是在呼叫靜態建構函式時初始化靜態變數,.NET執行時(CLR)能夠確保只調用一次靜態建構函式,這樣我們就能夠保證只初始化一次instance。

  餓漢式是在 .NET 中實現 Singleton 的首選方法。但是,由於在C#中呼叫靜態建構函式的時機不是由程式設計師掌控的,而是當.NET執行時發現第一次使用該型別的時候自動呼叫該型別的靜態建構函式(也就是說在用到LoadBalancer時就會被建立,而不是用到LoadBalancer.GetLoadBalancer()時),這樣會過早地建立例項,從而降低記憶體的使用效率。此外,靜態建構函式由 .NET Framework 負責執行初始化,我們對對例項化機制的控制權也相對較少。

 (2)懶漢式單例

  除了餓漢式之外,還有一種懶漢式。最開始我們實現的方式就是一種懶漢式單例,也就是說,在第一個呼叫LoadBalancer.GetLoadBalancer()時才會例項化物件,這種技術又被稱之為延遲載入(Lazy Load)。同樣,我們的目標還是為了避免多個執行緒同時呼叫GetLoadBalancer方法,在C#中,我們可以使用關鍵字lock/Moniter.Enter+Exit等來實現,這裡採用關鍵字語法糖lock來改寫程式碼段:

    public class LoadBalancer
    {
        // 私有靜態變數,儲存唯一例項
        private static LoadBalancer instance = null;
        private static readonly object syncLocker = new object();

        ......

        // 公共靜態成員方法,返回唯一例項
        public static LoadBalancer GetLoadBalancer()
        {
            if (instance == null)
            {
                lock (syncLocker)
                {
                    instance = new LoadBalancer();
                }
            }
            return instance;
        }
    }    

  問題貌似得以解決,但事實並非如此。如果使用以上程式碼來建立單例物件,還是會存在單例物件不一致。假設執行緒A先進入lock程式碼塊內,執行例項化程式碼。此時執行緒B排隊吃瓜等待,必須等待執行緒A執行完畢後才能進入lock程式碼塊。但當A執行完畢時,執行緒B並不知道例項已經被建立,將繼續建立新的例項,從而導致多個單例物件。因此,開發人員需要進一步改進,於是就有了雙重檢查鎖定(Double-Check Locking),其改寫程式碼如下:

    public class LoadBalancer
    {
        // 私有靜態變數,儲存唯一例項
        private static LoadBalancer instance = null;
        private static readonly object syncLocker = new object();

        ......

        // 公共靜態成員方法,返回唯一例項
        public static LoadBalancer GetLoadBalancer()
        {
            // 第一重判斷
            if (instance == null)
            {
                // 鎖定程式碼塊
                lock (syncLocker)
                {
                    // 第二重判斷
                    if (instance == null)
                    {
                        instance = new LoadBalancer();
                    }
                }
            }
            return instance;
        }
    }

 (3)一種更好的單例實現

  餓漢式單例不能延遲載入,懶漢式單例安全控制繁瑣,而且效能受影響。靜態內部類單例則將這兩者有點合二為一。使用這種方式,我們需要在單例類中增加一個靜態內部類,在該內部類中建立單例物件,再將該單例物件通過GetInstance()方法返回給外部使用,於是開發人員又改寫了程式碼:

    public class LoadBalancer
    {
        ......

        // 公共靜態成員方法,返回唯一例項
        public static LoadBalancer GetLoadBalancer()
        {
            return Nested.instance;
        }

        // 使用內部類+靜態建構函式實現延遲初始化
        class Nested
        {
            static Nested() { }
            internal static readonly LoadBalancer instance = new LoadBalancer();
        }

        ......
    }

  該實現方法在內部定義了一個私有型別Nested。當第一次用到這個巢狀型別的時候,會呼叫靜態建構函式建立LoadBalancer的例項instance。如果我們不呼叫屬性LoadBalancer.GetLoadBalancer()

,那麼就不會觸發.NET執行時(CLR)呼叫Nested,也就不會建立例項,因此也就保證了按需建立例項(或延遲初始化)。

  可見,此方法既可以實現延遲載入,又可以保證執行緒安全,不影響系統性能。但其缺點是與具體程式語言本身的特性相關,有一些面向物件的程式語言並不支援此種方式。

四、單例模式總結

  單例模式目標明確,結構簡單,在軟體開發中使用頻率相當高。

4.1 主要優點

  (1)提供了對唯一例項的受控訪問。單例類封裝了它的唯一例項,所以它可以嚴格控制客戶怎樣以及何時訪問它。

  (2)由於在系統記憶體中只存在一個物件,因此可以節約系統資源,對於一些需要頻繁建立和銷燬的物件,單例模式無疑可以提高系統的效能。

  (3)允許可變數目的示例。基於單例模式,開發人員可以進行擴充套件,使用與控制單例物件相似的方法來獲得指定個數的例項物件,既節省系統資源,又解決了單例物件共享過多有損效能的問題。(Note:自行提供指定書目的例項物件的類可稱之為多例類)例如,資料庫連線池,執行緒池,各種池。

4.2 主要缺點

  (1)單例模式中沒有抽象層,因此單例類的擴充套件有很大的困難。

  (2)單例類的職責過重,在一定程度上違背了單一職責的原則。因為單例類既提供了業務方法,又提供了建立物件的方法(工廠方法),將物件的建立和物件本身的功能耦合在一起。不夠,很多時候我們都需要取得平衡。

  (3)很多高階面向物件程式語言如C#和Java等都提供了垃圾回收機制,如果例項化的共享物件長時間不被利用,系統則會認為它是垃圾,於是會自動銷燬並回收資源,下次利用時又得重新例項化,這將導致共享的單例物件狀態的丟失。

4.3 適用場景

  (1)系統只需要一個例項物件。例如:系統要求提供一個唯一的序列號生成器或者資源管理器,又或者需要考慮資源消耗太大而只允許建立一個物件。

  (2)客戶呼叫類的單個例項只允許使用一個公共訪問點,除了該公共訪問點,不能通過其他途徑訪問該例項。

  比如,在Flappy Bird遊戲中,小鳥這個遊戲物件在整個遊戲中應該只存在一個例項,所有對於這個小鳥的操作(向上飛、向下掉等)都應該只會針對唯一的一個例項進行。

參考資料

      DesignPattern

  劉偉,《設計模式的藝術—軟體開發人員內功修煉之道》

  何海濤,《劍指Offer—名企面試官精講典型程式設計題》(題目1-實現Singleton模式)

作者:周旭龍

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連結。

相關推薦

設計模式征途1.Singleton模式

  單例模式屬於建立型模式的一種,建立型模式是一類最常用的設計模式,在軟體開發中應用非常廣泛。建立型模式將物件的建立和使用分離,在使用物件時無需關心物件的建立細節,從而降低系統的耦合度,讓設計方案更易於修改和擴充套件。每一個建立型模式都在檢視回答3個問題:3W -> 建立什麼(What)、由誰建立(Wh

【java設計模式】之 Singleton模式

1. 單例模式的定義         單例模式(Singleton Pattern)是一個比較簡單的模式,其原始定義如下:Ensure a class has only one instance, and provide a global point of access

小菜學習設計模式Singleton模式

前言 設計模式目錄: 本篇目錄: 簡單實現 執行緒安全 後記   單例模式(Singleton)可以說是最簡單的模式,對.net來說,因為不需要考慮到垃圾回收機制,實現起來很簡單,但是對於沒有提供記憶體管理的平臺來說,比如C++,因為單例模式只考慮建立物件,所以

Javasingleton模式

單例模式,是一種常用的軟體設計模式。在它的核心結構中只包含一個被稱為單例的特殊類。通過單例模式可以保證系統中,應用該模式的類一個類只有一個例項。即一個類只有一個物件例項。 單例模式的一個例子: 1、建立一個Singleton類 public class SingleO

Java Singleton模式

-s 避免 構造函數 new 類的構造函數 nbsp p s 載器 利用 一、什麽是單例模式: 單例模式是一種確保了一個類只有一個實例,而且自行實例化並向整個系統提供這個實例。被實例化的類稱為單例類。 二、單例模式的特點: 單例類只有一個實例。 單例類必須自行創

設計模式-Singleton

don 設計模式 static sha 應用 ces zed void 內部類 2018-1-12 by Atlas UML UML中加“-”表示私有的(private); UML中加“+”表示公有的(public); UML中加“_”表示靜態的(static)

設計模式4—— 建立型 ——Singleton

導航 首先通過懶漢式的單例模式簡單程式碼實現作為開頭,發現有執行緒安全問題,並且在此懶漢模式程式碼上進行改進,衍生出同步懶漢設計模式,雙重檢查懶漢設計模式。另外還有靜態內部類方式實現單例,它是一種基於類初始化的延遲載入解決方案。 與懶漢式相對應的是餓漢式單例模式,其在類載入時就進

設計模式4—— Singleton

導航 首先通過懶漢式的單例模式簡單程式碼實現作為開頭,發現有執行緒安全問題,並且在此懶漢模式程式碼上進行改進,衍生出同步懶漢設計模式,雙重檢查懶漢設計模式。另外還有靜態內部類方式實現單例,它是一種基於類初始化的延遲載入解決方案。 與懶漢式相對應的是餓漢式單例模式

[翻譯] Singleton

strip ble 不足 做的 時間 不能 att solution 事情 英文原文: https://sourcemaking.com/design_patterns/singleton 意圖 確保一個類只有一個實例,並提供一個訪問其實例的全局點; 封裝 “即時初始化

Singleton

1. 單例(Singleton) Intent 確保一個類只有一個例項,並提供該例項的全域性訪問點。 Class Diagram 使用一個私有建構函式、一個私有靜態變數以及一個公有靜態函式來實現。 私有建構函式保證了不能通過建構函式來建立物件例項,只能通過公有靜態函式返回唯一的私

Java Singleton

參考資料 [1]. 瘋狂Java講義(第三版) 李剛 單例(Singleton)類 雖然Java 系統允許,但是類成員還是不要訪問例項成員。 如果一個類始終只能建立一個例項,則這個類被稱為單

路一步步走>> 設計模式五:Singleton-單件

package com.test.DPs.ChuangJian.Singleton; /** * 建立型:Singleton-單例(單件) * * 單例模式-Singleton * 用途:保證一個類僅有一個例項,並提供一個訪問他的全域性訪問點。 */ public class Si

設計模式系列1——模式

設計模式是一種被重用的程式碼模式,主要大類分為三種:建立型模式、結構型模式、行為型模式。 單例模式的含義是:對於定義的一個類,在整個應用程式執行期間只有唯一的一個例項物件,這樣的設計模式叫做單例模式,單例模式分為餓漢式和懶漢式兩種。 一、懶漢式 懶漢式的特點是當需要用到此單

設計模式征途—23.解釋器Interpreter模式

args 參考資料 轉載 返回 下一個 tle title 缺點 images 雖然目前計算機編程語言有好幾百種,但有時人們還是希望用一些簡單的語言來實現特定的操作,只需要向計算機輸入一個句子或文件,就能按照預定的文法規則來對句子或文件進行解釋。例如,我們想要只輸入一個加法

模式之懶漢延遲初始化多執行緒再解析

單例模式之懶漢單例(延遲初始化)多執行緒再解析 1、多執行緒下的懶漢單例: public class Lazysingleton { private static Lazysingleton m_instance = null; // 私有預設構造方法

設計模式征途—22.中介者Mediator模式

我們都用過QQ,它有兩種聊天方式:一是私聊,二是群聊。使用QQ群,一個使用者就可以向多個使用者傳送相同的資訊和檔案,從而無需一一發送,節省大量時間。通過引入群的機制,極大地減少系統中使用者之間的兩兩通訊,使用者與使用者之間的聯絡可以通過群的機制來實現。 在有些軟體中,某些類/物件之間的相互呼叫關係錯綜複

設計模式征途—6.建造者Builder模式

建造者模式又稱為生成器模式,它是一種較為複雜、使用頻率也相對較低的建立型模式。建造者模式為客戶端返回的不是一個簡單的產品,而是一個由多個部件組成的複雜產品。因為,沒有人買車會只買一個方向盤或者輪胎,大家買的都是一輛包含輪胎、方向盤和發動機等多個部件組成的完整汽車。如何將這些部件組裝成一輛完整的汽車並返回給使用

設計模式征途—15.觀察者Observer模式

在日常生活中,交通訊號燈指揮者日益擁擠的城市交通。紅燈亮,汽車停止;綠燈亮,汽車繼續前行;在這個過程中,交通訊號燈是汽車的觀察目標,而汽車則是觀察者。隨著交通訊號燈的變化,汽車的行為也會隨之變化,一盞交通訊號燈可以指揮多輛汽車。在軟體系統中,有些物件之間也存在類似交通訊號燈和汽車之間的關係,一個物件的狀態或行

設計模式征途—8.橋接Bridge模式

在現實生活中,我們常常會用到兩種或多種型別的筆,比如毛筆和蠟筆。假設我們需要大、中、小三種類型的畫筆來繪製12中不同的顏色,如果我們使用蠟筆,需要準備3*12=36支。但如果使用毛筆的話,只需要提供3種型號的毛筆,外加12個顏料盒即可,涉及的物件個數僅為3+12=15,遠遠小於36卻能實現與36支蠟筆同樣的功

設計模式征途—12.享元Flyweight模式

現在在大力推行節約型社會,“浪費可恥,節儉光榮”。在軟體系統中,有時候也會存在資源浪費的情況,例如,在計算機記憶體中儲存了多個完全相同或者非常相似的物件,如果這些物件的數量太多將導致系統執行代價過高。那麼,是否存在一種技術可以用於節約記憶體使用空間,實現對這些相同或者相似物件的共享訪問呢?答案是肯定的,這種技