設計模式之單例模式

 

 

1、引子

 

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

1.        單窗體的問題。

在主應用程序菜單點擊菜單,彈出工具箱窗體,如今的問題是,但願工具箱要麼不出現,出現也只能夠出現一個,可是實際上每次點擊菜單,都會實例化一個「工具箱」並顯示出來,這樣會產生不少個「工具箱」,不是所但願的。注意這裏但願的是「工具箱」窗體單例,而不是進程單個實例(進程單個實例:例如PC上已經打開一個迅雷,再次運行迅雷,結果並無再開一個迅雷而仍是以前的,區分同一PC登錄多個QQ客戶端)。數據庫

     

如上圖,每次單擊菜單都會實例化一個工具箱窗體,與指望不符。編程

 

2. 大對象問題

 

對象有保存對象狀態信息的一些字段,字段過多或者字段自己佔據大量內存,都會致使對象過大。下面看一段示例:windows

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,按回車鍵繼續建立另一個對象,此時內存翻倍增長至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();        
        }

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

 

3、      單例模式

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判斷,代碼更加精煉,又能避免加鎖解鎖。

 

4、      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;

P一、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機制

 

RAIIResource 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++各個版本的實現和本身的理解,若有錯誤,請你們指正。

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

相關文章
相關標籤/搜索