設計模式之單例模式


 

 

一、引子

 

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

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,按回車鍵繼續創建另外一個對象,此時內存翻倍增加至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機制

 

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++各個版本的實現和自己的理解,如有錯誤,請大家指正。

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM