天天看點

設計模式之單例模式

一、引子

首先來看兩個常見的問題:

1.        單窗體的問題。

在主應用程式菜單點選菜單,彈出工具箱窗體,現在的問題是,希望工具箱要麼不出現,出現也隻可以出現一個,但是實際上每次點選菜單,都會執行個體化一個“工具箱”并顯示出來,這樣會産生很多個“工具箱”,不是所希望的。注意這裡希望的是“工具箱”窗體單例,而不是程序單個執行個體(程序單個執行個體:例如PC上已經打開一個迅雷,再次運作迅雷,結果并沒有再開一個迅雷而還是之前的,區分同一PC登陸多個QQ用戶端)。

設計模式之單例模式

如上圖,每次單擊菜單都會執行個體化一個工具箱窗體,與期望不符。

2. 大對象問題

對象有儲存對象狀态資訊的一些字段,字段過多或者字段本身占據大量記憶體,都會導緻對象過大。下面看一段示例:

class SimpleLargeObject
    {
        private const int NUM = 100 * 1024 * 1024;//100MB
        private byte[] data = null;

        public SimpleLargeObject()
        {
            data = new byte[NUM];
            for (int i = 0; i < data.Length; i++)
            {
                data[i] = (byte)(i % 255);
            }
        }

        public void Method1()
        {
            Console.WriteLine("Method1");
        }

        // other methods....

    }

    class Program
    {
        static void Main(string[] args)
        {
            SimpleLargeObject obj1=new SimpleLargeObject();
            obj1.Method1();
            Console.WriteLine("Press enter to create a new object...");
            Console.ReadLine();
            SimpleLargeObject obj2 = new SimpleLargeObject();
            obj2.Method1();
            Console.ReadLine();
        }
    }

      

為了更展現出問題,這裡誇張一點,SimpleLargeObject占據記憶體100MB。

設計模式之單例模式
設計模式之單例模式

運作發現記憶體占據100MB,按Enter鍵繼續建立另外一個對象,此時記憶體翻倍增加至200MB…   可以想象,當特定環境下需要産生無數個對象,而這些對象本身的狀态資訊由私有字段來維護,字段的取值不同會影響到公開方法的行為,而這些對象又不需要在同一時刻都要存在,或者無數個這樣的對象狀态資訊無關緊要,産生這麼多對象會導緻記憶體占用過多。

對于第一個問題,正常解決方法是在調用窗體類中聲明一個ToolBoxForm類型的全局,判斷這個ToolBoxForm類型的全局變量是否執行個體化過就行了。

private ToolBoxForm toolBoxForm = null;
        private void toolStripMenuItemToolBox_Click(object sender, EventArgs e)
        {
            if (toolBoxForm == null)
            {
                toolBoxForm = new ToolBoxForm();
                toolBoxForm.Show();
            }
        }      

  這樣似乎解決問題了。

  新需求來了:現在不但要在菜單裡面啟動“工具箱”,還需要在“工具欄”上的按鈕來快捷啟動“工具箱”。菜單欄有些常用的功能提供快捷按鈕再正常不過的需求了。

設計模式之單例模式

  這個不難,增加一個工具欄控件,然後添加onclick事件,複制同樣的代碼就行了:

private void toolStripButton1_Click(object sender, EventArgs e)
        {
            if (toolBoxForm == null)
            {
                toolBoxForm = new ToolBoxForm();
                toolBoxForm.Show();
            }
        }      

  複制代碼潛在的問題也是很明顯的:

  1. 一份代碼多出重複,如果需求變化或者有BUG時就需要改多個地方。如果有5個地方需要執行個體化“工具箱”窗體,這個小bug就需要改動5個地方,可見複制粘貼多麼害人。
  2. 複制粘貼是最容易的程式設計,也是最沒有價值的程式設計,隻求達到目标,如何能有提高。

上面的程式就有潛在的Bug,啟動“工具箱”,然後把“工具箱”窗體關閉,再點啟動按鈕,問題就暴露出來了。原因是關閉“工具箱”窗體時,它的執行個體并沒有變為null,而隻是Disposed。

Form.Show()方法出的窗體,關閉調用Close()會Dispose記憶體,對象銷毀,但指向對象的引用不為null;

Form.ShowDilog()方法出的窗體,關閉窗體不會釋放對象的記憶體,窗體的引用也不為null,窗體隻是hidden而已。

上述Bug修複,并重構提煉方法後的代碼:

private ToolBoxForm toolBoxForm = null;
        private void toolStripMenuItemToolBox_Click(object sender, EventArgs e)
        {
            OpenToolBox();
        }

        private void toolStripButton1_Click(object sender, EventArgs e)
        {
            OpenToolBox();
        }

        private void OpenToolBox()
        {
            if (toolBoxForm == null||toolBoxForm.IsDisposed)
            {
                toolBoxForm = new ToolBoxForm();
                toolBoxForm.Show();
            }
        }      

  現在基本沒什麼問題了。

二 .類的職責

在上面幾步的優化和改善,已經基本沒什麼問題了,但是這樣做“工具箱”是否執行個體化都是在調用顯示“工具箱”的地方來判斷,這樣不符合邏輯,主窗體裡面應該隻是通知啟動“工具箱”,至于“工具箱”窗體是否執行個體化過,主窗體根本不關心,這不屬于主窗體的職責,“工具箱”是否執行個體化過,應該有“工具箱”自己來判斷。對象是否執行個體化是它自己的責任,而不是别人的責任,别人隻是使用它就可以了。

對象的執行個體化其實就是new的過程,如果要控制對象的執行個體化由該類自身來維護,那麼類的構造函數應該是私有的,這樣外部就不能用new來執行個體化它了,而讓這個類隻能執行個體化一次,用靜态的類變量能達到目的,因為靜态是該類型共享的,而該類型剛好是這個類本身。

設計模式之單例模式

   用戶端使用的代碼:

private void toolStripMenuItem1_Click(object sender, EventArgs e)
        {
            ToolBoxForm.Instance.Show();
        }

        private void toolStripButton1_Click(object sender, EventArgs e)
        {
            ToolBoxForm.Instance.Show();        
        }      

這樣一來,用戶端不再考慮是否需要去執行個體化的問題,而把責任都給了應該負責的類去處理。這就是一個很根本的設計模式:單例模式。

三、      單例模式

1.       基本的單例

定義:保證一個類僅有一個執行個體,并提供一個通路它的全局通路點。——GOF的《設計模式:可複用面向對象軟體的基礎》

通常我們可以讓一個全局變量使得一個對象被通路,但它不能防止你執行個體化多個對象。最好的辦法就是,讓類自身負責儲存它的唯一執行個體。這個類可以保證沒有其他執行個體可以被建立,并且可以提供一個通路該執行個體的方法。

class Singleton
    {
        private static Singleton instance;

        private Singleton() //構造方法為private,這就堵死了外界利用new建立此類型執行個體的可能
        {
        }

        public static Singleton GetInstance() //次方法是獲得本類執行個體的唯一全局通路點
        {
            if (instance == null)
            {
                instance = new Singleton();
            }

            return instance;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
           // Singleton s0 = new Singleton();//錯誤,外界不能通過new來建立此類型執行個體
            Singleton s1 = Singleton.GetInstance();
            Singleton s2 = Singleton.GetInstance();
            if (s1 == s2)
            {
                Console.WriteLine("兩個對象是相同的執行個體");
            }

            Console.ReadLine();
        }
 }      

 運作結果,s1和s2是同一個執行個體,都是通過唯一的全局通路點Singleton.GetInstance()方法傳回的。

2.       多線程環境下的單例

先模拟一個多線程的環境:

class Singleton
    {
        private static Singleton instance;

        private Singleton() //構造方法為private,這就堵死了外界利用new建立此類型執行個體的可能
        {
           Thread.Sleep(50);//此處模拟建立對象耗時
        }

        public static Singleton GetInstance() //次方法是獲得本類執行個體的唯一全局通路點
        {
            if (instance == null)
            {
                instance = new Singleton();
            }

            return instance;
        }
    }

    class Program
    {
        const int THREADCOUNT = 200;
        static List<Singleton> sList = new List<Singleton>(THREADCOUNT);
        static object objLock = new object();
      
        static void Main(string[] args)
        {
            Task[] tasks=new Task[THREADCOUNT];

            for (int i = 0; i < THREADCOUNT; i++)
            {
                tasks[i] = Task.Factory.StartNew(ThredFunc);
            }
           
            Task.WaitAll(tasks);//確定所有任務執行完畢
            Console.WriteLine("sList.Count:" + sList.Count);

            int index1 = -1;
            int index2 = -1;
            if(HasDifferentInstance(out index1,out index2))
            {
                Console.WriteLine("含有不相同的執行個體,index1={0},index2={1}", index1, index2);
            }
            

            Console.WriteLine("執行完畢.");
            Console.ReadLine();
            
        }

        private static bool HasDifferentInstance(out int index1,out int index2)
        {
            index1 = index2 = -1;
            for (int i = 0; i < sList.Count; i++)
            {
                for (int j = i + 1; j < sList.Count - 1; j++)
                {
                    if (sList[i] != sList[j])
                    {                    
                        index1 = i;
                        index2 = j;
                        return true;
                        
                    }
                }
            }
            return false;
        }

        private static void ThredFunc()
        {
            Singleton singleton = Singleton.GetInstance();
            lock (objLock)
            {
                sList.Add(Singleton.GetInstance());
            }
        }      
設計模式之單例模式

我們在Singleton的構造函數延遲50ms來模拟建立對象耗時,這樣在多線程的環境下,很容易出現在一個線程執行Singleton.GetInstance()時建立對象,而這個對象的建立理論上是要消耗時間的,在建立對象之前instance為null,還未傳回,此時另一個線程也執行Singleton.GetInstance()判斷instance為null,執行了new建立了對象,這樣出現了對象執行個體不為同一個對象的情況。

為了解決這個問題,在執行new建立執行個體的地方加上鎖,同時在鎖定之前判斷下是否為null,這樣如果已經建立就不用進入鎖了。

public static Singleton GetInstance() //次方法是獲得本類執行個體的唯一全局通路點
        {
            if (instance == null)
            {
                lock (objLock)
                {
                    if (instance == null)
                    {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }      

對于instance存在的情況,就直接傳回;當instance為null并且同時有兩個線程GetInstance()方法時,它們都可以通過第一重instance==null的判斷,然後由于lock機制,這兩個線程則隻有一個進入,另一個在排隊等候,必須要其中的一個進入并出來後,另一個才能進入。而此時如果沒有了第二重的instance是否為null的判斷,則第一個線程建立了執行個體,而第二個線程還是可以繼續再建立新的執行個體,是以需要兩次判斷。

進行一次加鎖和解鎖是需要付出對應的代價的,而進行兩次判斷,就可以避免多次加鎖與解鎖操作,同時也保證了線程安全。但是,這種實作方法在平時的項目開發中用的很好,也沒有什麼問題?但是,如果進行大資料的操作,加鎖操作将成為一個性能的瓶頸;為此,一種新的單例模式的實作也就出現了。

   上面的Doule-Check Locking(雙重鎖定) 能進一步優化,利用CLR類型構造器保證線程安全:

class Singleton
    {
        private static Singleton instance;

        static Singleton()  //類型構造器,確定線程安全
        {
            instance = new Singleton();
        }

        private Singleton() //構造方法為private,這就堵死了外界利用new建立此類型執行個體的可能
        {
           Thread.Sleep(50);//此處模拟建立對象耗時
        }

        public static Singleton GetInstance() //次方法是獲得本類執行個體的唯一全局通路點
        {          
            return instance;
        }
    }      

不需要null判斷,代碼更加精煉,又能避免加鎖解鎖。

四、      C++ 單例模式

盡管單例模式的思想是一緻的,但是C++ 與C#有很多不同點,甚至有時候用到語言平台的獨有特性有意想不到的效果,例如利用CLR的特性,類型構造器能確定線程安全性。這裡介紹一下C++實作單例模式。 利用GOF中單例模式的定義,很容易寫出如下的代碼:

版本一:

class Singleton
{
private:
    Singleton()
    {
    }
    static Singleton * m_pInstance;

public:
    static Singleton * GetInstance()
    {
        if (m_pInstance == NULL)
        {
            m_pInstance = new Singleton();
        }
        return m_pInstance;
    }
};
Singleton * Singleton::m_pInstance = NULL;      

使用者通路唯一執行個體的方法隻有GetInstance()成員函數。如果不通過這個函數,任何建立執行個體的嘗試都将失敗,因為類的構造函數是私有的。GetInstance()使用懶惰初始化,也就是說它的傳回值是當這個函數首次被通路時被建立的,所有GetInstance()之後的調用都傳回相同執行個體的指針:

Singleton *p1 = Singleton::GetInstance();
 Singleton *p2 = Singleton::GetInstance();
 Singleton *p3 = p2;      

P1、p2都是通過GetInstance()全局通路點通路的,指向的是同一執行個體,p3是經過指針指派,也是指向同一執行個體,它們的位址相同:

設計模式之單例模式

大多數時候,這樣的實作都不會出現問題。有經驗的讀者可能會問,m_pInstance指向的空間什麼時候釋放呢?這樣會不會導緻記憶體洩漏呢?

我們一般的程式設計觀念是,new操作是需要和delete操作進行比對的;是的,這種觀念是正确的。具體看場景。static Singleton * m_pInstance;m_pInstance 指針本身為靜态的,存儲方式為靜态存儲,生命周期為程序周期;而其指向的執行個體對象在堆上配置設定,這個堆對象有個特點就是隻有一個執行個體,堆記憶體由程式員釋放或程式結束時可能由OS回收。

堆區(heap) — 一般由程式員配置設定釋放, 若程式員不釋放,程式結束時可能由OS回收 。

注意,這裡是可能。具體能不能得看OS,目前windows是可以的,而嵌入式系統有些是不能的。是以還得看場景。

在實際項目中,特别是用戶端開發,其實是不在乎這個執行個體的銷毀的。因為,盡管這個指向執行個體的指針為靜态的,而這個執行個體為堆中對象并且隻有一個,程序結束後,它會釋放它占用的記憶體資源的,是以,也就沒有所謂的記憶體洩漏了。而針對服務端程式,一般是長期運作,但是這個執行個體也隻有一個,程序結束,作業系統會回收記憶體。

顯然,把記憶體回收的責任交給OS,雖然大多數情況下是沒問題的,但是還是看場景的,記憶體能不能回收也取決于OS核心。

更重要的是,在以下情形,是必須需要進行執行個體銷毀的:

在類中,有一些檔案鎖了,檔案句柄,資料庫連接配接等等,這些随着程式的關閉而不會立即關閉的資源,必須要在程式關閉前,進行手動釋放;

版本二:添加手動釋放函數

class Singleton
{
private:
    Singleton()
    {
    }
    static Singleton * m_pInstance ;

public:
    static Singleton * GetInstance()
    {
        if (m_pInstance == NULL)
        {
            m_pInstance = new Singleton();
        }
        return m_pInstance;
    }

    static void DestoryInstance()
    {
        if (m_pInstance != NULL)
        {
            delete m_pInstance;
            m_pInstance = NULL;
        }
    }
};      

   我們單例類中添加一個DestoryInstance()函數來删除執行個體,可以在程序退出之前來調用這個函數釋放,結合前面“類的職責”小結,很快會發現這樣不是很優雅,理想情況下是類的使用者隻管拿來用,而不用關注什麼時候釋放,并且程式員忘了調用這個函數也是很容易發生的事。能不能實作像boost中shared_ptr<T>這樣自動釋放記憶體呢?

由于這個執行個體的生命周期為直到程序結束,是以可以設計一個包裝類作為靜态變量,靜态變量的生命周期也是到程序結束銷毀,可以在這個包裝類的析構函數裡面釋放資源。

以下是改進版本:

版本三:利用RAII自動釋放

class Singleton
{
private:
    Singleton()
    {
    }
    static Singleton * m_pInstance ;

    class GC //内部包裝類
    {
    public:
        ~GC()
        {
            if (m_pInstance != NULL)
            {
                std::cout << "Here is the test,delete m_pInstance." << std::endl;
                delete m_pInstance;
                m_pInstance = NULL;
            }
        }
    };

    static GC m_gc;
public:
    static Singleton * GetInstance()
    {
        if (m_pInstance == NULL)
        {
            m_pInstance = new Singleton();
        }
        return m_pInstance;
    }

};

Singleton * Singleton::m_pInstance = NULL;//這裡初始化Singleton的靜态成員m_pInstance
Singleton::GC Singleton::m_gc;//這裡初始化Singleton裡面嵌套類GC的靜态成員m_gc

int _tmain(int argc, _TCHAR* argv[])
{
    
    Singleton *p1 = Singleton::GetInstance();
    Singleton *p2 = Singleton::GetInstance();
    std::cin.get();
    return 0;
}      

運作程式,執行到cin.get()後敲回車,程式即将退出,輸出以下結果:

設計模式之單例模式

說明嵌套類GC的析構函數已經執行。此處使用了一個内部GC類,而該類的作用就是用來釋放資源,其定義在Singleton的private部分,外部無法通路,也不關心。程式在結束的時候,系統會自動析構所有的全局變量,實際上,系統也會析構所有類的靜态成員變量,就像這些靜态變量是全局變量一樣。我們知道,靜态變量和全局變量在記憶體中,都是存儲在靜态存儲區的,是以在析構時,是同等對待的。在程式運作結束時,系統會調用Singleton的靜态成員static GC m_gc的析構函數,該析構函數會進行資源的釋放,而這種資源的釋放方式是在程式員“不知道”的情況下進行的,而程式員不用特别的去關心,使用單例模式的代碼時,不必關心資源的釋放。這裡運用了C++中的RAII機制。

RAII是Resource Acquisition Is Initialization的簡稱,是C++語言的一種管理資源、避免洩漏的慣用法。利用的就是C++構造的對象最終會被銷毀的原則。RAII的做法是使用一個對象,在其構造時擷取對應的資源,在對象生命期内控制對資源的通路,使之始終保持有效,最後在對象析構的時候,釋放構造時擷取的資源。

            前面的各個版本還沒考慮多線程的問題,參考前面C#版本的“雙檢鎖”,而C++語言本身不提供多線程支援的,多線程的實作是由作業系統提供支援的,可以用系統API。這裡用

C++ 0x 的線程庫,C++ 0x裡面部分庫由boost發展而來。

版本四: 多線程環境下“雙檢鎖”

class Singleton
{
private:
    Singleton()
    {
    }
    static Singleton * m_pInstance;
    class GC //内部包裝類
    {
    public:
        ~GC()
        {
            if (m_pInstance != NULL)
            {
                std::cout << "Here is the test,delete m_pInstance." << std::endl;
                delete m_pInstance;
                m_pInstance = NULL;
            }
        }
    };

    static GC m_gc;
    static std::mutex m_mutex;
public:
    static Singleton * GetInstance()
    {
        if (m_pInstance == NULL)
        {
            m_mutex.lock();
            if (m_pInstance == NULL)
            {
                m_pInstance = new Singleton();
            }
            m_mutex.unlock();
        }
        return m_pInstance;
    }

};

Singleton * Singleton::m_pInstance = NULL;//這裡初始化Singleton的靜态成員m_pInstance
Singleton::GC Singleton::m_gc;//這裡初始化Singleton裡面嵌套類GC的靜态成員m_gc
std::mutex Singleton::m_mutex; //初始化Singleton靜态成員m      

這裡使用了C++ 0x的mutex,需要#include <mutex>

繼續參考之前C#版本的優化,提供靜态初始化版本:

版本五:靜态初始化

class Singleton
{
private:
    Singleton()
    {
    }
    const static Singleton * m_pInstance;
    class GC //内部包裝類
    {
    public:
        ~GC()
        {
            if (m_pInstance != NULL)
            {
                std::cout << "Here is the test,delete m_pInstance." << std::endl;
                delete m_pInstance;
                m_pInstance = NULL;
            }
        }
    };

    static GC m_gc;
public:
    static Singleton * GetInstance()
    {        
        return const_cast<Singleton *>(m_pInstance);
    }

    void TestMethod()
    {
        std::cout << "Singleton::TestMethod" << std::endl;
    }
};

const Singleton* Singleton::m_pInstance = new Singleton(); //這裡靜态初始化
Singleton::GC Singleton::m_gc;//這裡初始化Singleton裡面嵌套類GC的靜态成員m_gc
int _tmain(int argc, _TCHAR* argv[])
{

    Singleton *p1 = Singleton::GetInstance();
    Singleton *p2 = Singleton::GetInstance();
    p1->TestMethod();
    std::cin.get();
    return 0;
}      

因為靜态初始化在程式開始時,也就是進入主函數之前,由主線程以單線程方式完成了初始化,是以靜态初始化執行個體保證了線程安全性。在性能要求比較高時,就可以使用這種方式,進而避免頻繁的加鎖和解鎖造成的資源浪費。

語言特性

下面我們看看其它版本,先不考慮多線程(多線程問題前面讨論過了,不做重點,也可以在主函數之前以單線程方式先完成初始化來達到目的)。

class Singleton
{
private:
    Singleton()
    {
    }
public:
    static Singleton&  GetInstance()
    {
        static Singleton instance;
        return instance;
    }
    void TestMethod()
    {
        std::cout << "Singleton::TestMethod()" << std::endl;
    }
};      

這個版本不再使用指針,而是傳回一個靜态局部變量的引用。也許有人會問,傳回局部變量的引用,局部變量過了作用域就析構了啊,但是注意這裡是靜态局部變量,存儲

方式為靜态存儲,生命周期為到程序退出,是以不用擔心函數結束就析構了。C# 和Java等沒有靜态局部變量的概念,這個可以說是C/C++的一個特性。

寫程式測試:

int _tmain(int argc, _TCHAR* argv[])
{
    
    Singleton::GetInstance().TestMethod();
    Singleton s1= Singleton::GetInstance();
    Singleton s2 = s1;
    if (addressof(s1) == addressof(s2))
    {
        cout << "同一執行個體" << endl;
    }
    else
    {
        cout << "不同執行個體" << endl;
        cout <<"s1的位址:"<<(int)(&s1) << endl;
        cout <<"s2的位址:" <<(int)(&s2) << endl;
    }
    std::cin.get();
    return 0;
}      
設計模式之單例模式

發現s1和s2是不同的執行個體,這是因為對象的建立除了構造函數外還有其他方式,例如複制構造函數、指派操作符等,都需要禁止。

改進版本:

class Singleton
{
private:
    Singleton()
    {
    }
    Singleton(const Singleton&) = delete;//禁止複制
    Singleton operator=(const Singleton&) = delete;//禁止指派操作
public:
    static Singleton&  GetInstance()
    {
        static Singleton instance;
        return instance;
    }
    void TestMethod()
    {
        std::cout << "Singleton::TestMethod()" << std::endl;
    }
};      

這樣,外部企圖通過指派操作符或者複制來建立對象,都會報錯:

設計模式之單例模式

Singleton::GetInstance() 是唯一的全局通路點和通路方式。

項目中出現多個需要用到單例的類怎麼辦?分别編寫禁止複制構造函數、禁止指派操作,分别編寫GetInstance()方法 這種重複的工作?我們宏可以解決這個重複性工作:

#define  SINGLINTON_CLASS(class_name) \
    private:\
    class_name(){}\
    class_name(const class_name&);\
    class_name& operator = (const class_name&);\
    public:\
    static class_name& Instance()\
    {\
      static class_name one;\
      return one;\
    }


class Simple
{
    SINGLINTON_CLASS(Simple)

public:
    void Print()
    {
        cout<<"Simple::Print()"<<endl;
    }
};      

可以把上面的宏寫到一個頭檔案中,在需要寫單例的地方include這個頭檔案,單例類開頭隻需加上SINGLINTON_CLASS(class_name)就行了,其中class_name為目前類名,然後可以講工作重心放到這個類的設計上。

客戶的還是照樣調用:

int _tmain(int argc, _TCHAR* argv[])
{
    Simple::Instance().Print();
    
    cin.get();
    return 0;
}      

總結

單例模式可以說是設計模式裡面最基本和簡單的一種了,為了寫這篇文章,自己調查了很多方面的資料,例如《大話設計模式》,同時加上C++各個版本的實作和自己的了解,如有錯誤,請大家指正。

在實際的開發中,并不會用到單例模式的這麼多種版本,每一種設計模式,都應該在最适合的場合下使用,在日後的項目中,應做到有地放矢,而不能為了使用設計模式而使用設計模式。