首先來看兩個常見的問題:程序員
在主應用程序菜單點擊菜單,彈出工具箱窗體,如今的問題是,但願工具箱要麼不出現,出現也只能夠出現一個,可是實際上每次點擊菜單,都會實例化一個「工具箱」並顯示出來,這樣會產生不少個「工具箱」,不是所但願的。注意這裏但願的是「工具箱」窗體單例,而不是進程單個實例(進程單個實例:例如PC上已經打開一個迅雷,再次運行迅雷,結果並無再開一個迅雷而仍是以前的,區分同一PC登錄多個QQ客戶端)。數據庫
如上圖,每次單擊菜單都會實例化一個工具箱窗體,與指望不符。編程
對象有保存對象狀態信息的一些字段,字段過多或者字段自己佔據大量內存,都會致使對象過大。下面看一段示例: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(); } }
複製代碼潛在的問題也是很明顯的:
上面的程序就有潛在的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(); }
這樣一來,客戶端再也不考慮是否須要去實例化的問題,而把責任都給了應該負責的類去處理。這就是一個很根本的設計模式:單例模式。
定義:保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。——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()方法返回的。
先模擬一個多線程的環境:
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#有不少不一樣點,甚至有時候用到語言平臺的獨有特性有意想不到的效果,例如利用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機制。
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++各個版本的實現和本身的理解,若有錯誤,請你們指正。
在實際的開發中,並不會用到單例模式的這麼多種版本,每一種設計模式,都應該在最適合的場合下使用,在往後的項目中,應作到有地放矢,而不能爲了使用設計模式而使用設計模式。