設計模式Singleton

 
引言
相信大多數拜讀過"Gang Of Four"(Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides)的經典之作《Design Pattern》的同僚們,對這本書一定推崇有加。曾有人這麼宣告:"只有在讀過《Design Pattern》後,我的編程水平才真正得到了質的飛躍。"
那麼,如何才能步入設計模式的殿堂?設計模式是資深程序員日積月累總結出來的一套可複用的、針對面向對象軟件設計的解決方案,從這個意義上說,世界上存在無數多的設計模式,"Gang Of Four"總結的23種設計模式只是其中的23個精華。入手的關鍵就在於領會"設計模式"的思想,然後再將它們融會貫通、靈活應用到自己到開發過程中。

Singleton模式
Singleton可以說是《Design Pattern》中最簡單也最實用的一個設計模式。那麼,什麼是Singleton?
顧名思義,Singleton就是確保一個類只有唯一的一個實例。Singleton主要用於對象的創建,這意味着,如果某個類採用了Singleton模式,則在這個類被創建後,它將有且僅有一個實例可供訪問。很多時候我們都會需要Singleton模式,最常見的比如我們希望整個應用程序中只有一個連接數據庫的Connection實例;又比如要求一個應用程序中只存在某個用戶數據結構的唯一實例。我們都可以通過應用Singleton模式達到目的。
一眼看去,Singleton似乎有些像全局對象。但是實際上,並不能用全局對象代替Singleton模式,這是因爲:其一,大量使用全局對象會使得程序質量降低,而且有些編程語言例如C#,根本就不支持全局變量。其二,全局對象的方法並不能阻止人們將一個類實例化多次:除了類的全局實例外,開發人員仍然可以通過類的構造函數創建類的多個局部實例。而Singleton模式則通過從根本上控制類的創建,將"保證只有一個實例"這個任務交給了類本身,開發人員不可能再有其它途徑得到類的多個實例。這一點是全局對象方法與Singleton模式的根本區別。

Singleton模式的實現
Singleton模式的實現基於兩個要點:
1)不直接用類的構造函數,而另外提供一個Public的靜態方法來構造類的實例。通常這個方法取名爲Instance。Public保證了它的全局可見性,靜態方法保證了不會創建出多餘的實例。
2)將類的構造函數設爲Private,即將構造函數"隱藏"起來,任何企圖使用構造函數創建實例的方法都將報錯。這樣就阻止了開發人員繞過上面的Instance方法直接創建類的實例。
通過以上兩點就可以完全控制類的創建:無論有多少地方需要用到這個類,它們訪問的都是類的唯一生成的那個實例。
 
戲說Singleton模式

GOF著作中對Singleton模式的描述爲:保證一個class只有一個實體(Instance),併爲它提供一個全局訪問點(global access point)。

從其描述來看,是非常簡單的,但實現該模式卻是複雜的。Singleton設計模式不存在一種所謂的“最佳”方案。需要根據當時的具體問題進行具體解決,下面將講述在不同環境下的解決方案。

Singleton的詳細解釋,請大家看GOF的著作《設計模式》一書。俺比較懶,是不想抄了。J

1         Singleton創建

1.1      GOF Singleton

在GOF著作中對Singleton模式的實現方式如下:

/*解一*/

class Singleton

{

public:

static Singleton *Instance(){                            //1

if( !m_pInstatnce) //2

m_pInstance = new Singleton;//3

return m_pInstance; //4

}

private:

static Singleton *m_pInstatnce;             //5

private:

Singleton();                                                         //6

Singleton(const Singleton&);                             //7

Singleton& operator=(const Singleton&);            //8

~Singleton();                                                       //9

}

Singleton *Singleton:m_pInstatnce = NULL; //10

 

在上面的解決方案中,我們只在需要調用時,才產生一個Singleton的對象。這樣帶來的好處是,如果該對象產生帶來的結果很昂貴,但不經常用到時,是一種非常好的策略。但如果該Instance被頻繁調用,那麼就有人覺得Instance中的判斷降低了效率(雖然只是一個判斷語句^_^),那麼我們就把第5條語句該爲

static Singleton m_Instatnce;

如此一來,在Instatnce直接返回&m_Instance,而不用做任何判斷,效率也高了。(是不是呢?)

這樣修改後,我們將帶來災難性的後果:

1:首先有可能編譯器這關就沒法通過,說m_Instance該外部變量無法解決(visural C++6.0)

error LNK2001: unresolved external symbol "private: static class Singleton  Singleton::m_Instance" (?m_Instance@Singleton@@0V1@A)

2:如果編譯器這關通過了就沒問題了麼?答案是否定的。

第一是不管Instance是否用到,該靜態變量對象在編譯器編譯時就產生了,即資源消耗是不可避免的;

第二是無法確保編譯器一定先將m_Instance初始化。所以Instance的調用有可能傳回一個尚沒構造的Singleton對象。這也意味着你無法保證任何外部對象所使用的m_Instance是一個被正確初始化的對象。

1.2      Meyers Singleton

我們如何解決這個問題呢,實際上很簡單。一種非常優雅的做法由Scott Meyers最先提出,故也稱爲Meyers Singleton。它依賴編譯器的神奇技巧。即函數內的static對象只在該函數第一次執行時才初始化(請注意不是static常量)。

/*解二*/

class Singleton

{

public:

static Singleton *Instance(){                            //1

static Singleton sInstance; //2

return &sInstance; //3

}

private:

Singleton();                                                         //4

Singleton(const Singleton&);                             //5

Singleton& operator=(const Singleton&);            //6

~Singleton();                                                       //7

}

 

解二在Instance中定義了一個Static的Singleton對象,來解決Instance中初始化的問題,也很順利的解決了定義Static成員對象帶來的問題。

請注意,解二在VC6中不能編譯通過,將有以下的錯誤:

error C2248: 'Singleton::~Singleton' : cannot access private member declared in class 'Singleton' e:\work\q\a.h(81) : see declaration of 'Singleton::~Singleton'

產生該問題的錯誤原因是什麼呢(請仔細思考^_^)

原因在於在產生static Singleton對象後,編譯器會自動產生一個銷燬函數__DestroySingleton,然後調用atexit()註冊,在程序退出時執行__DestroySingleton。但由於Singleton的析構函數是private,所以會產生訪問錯誤。(應該在以後的編譯器中修改了該BUG)

1.3      Singleton改進

讓Instance傳回引用(reference)。如果傳回指針,調用端有可能講它delete調。

1.4      Singleton注意之點

在上面的解法中,請注意對構造函數和析構函數的處理,有何好處(請自己理解,俺懶病又犯了L)。

2         多線程

在解一中,如果我們運行在多線程的環境中,該方案是完美的麼,將會有什麼後果呢?

後果就是會造成內存泄漏,並且有可能前後獲取的Singleton對象不一樣(原因請自己思考,後面有解答)。

爲了解決這個問題,將解一的Instance改爲如下:

Singleton& Singleton::Instance(){

Lock(m_mutex);            //含義爲獲取互斥量            //1

If( !m_pInstance ){                                          //2

m_pInstance = new Singleton; //3

}

UnLock(m_mutex);                                            //4

return *m_pInstance;                                     //5

}

此種方法將解決解一運行在多線程環境下內存泄漏的問題,但帶來的結果是,當m_mutex被鎖定時,其它試圖鎖定m_mutex的線程都將必須等等。並且每次執行鎖操作其付出的代價極大,亦即是這種方案的解決辦法並不吸引人。

那麼我們將上面的代碼改爲如下方式:

Singleton& Singleton::Instance(){

If( !m_pInstance ){                                                      //1

Lock(m_mutex); //含義爲獲取互斥量 //2

m_pInstance = new Singleton; //3

UnLock(m_mutex); //4

}

return *m_pInstance;                                                 //5

}

這樣修改的結果沒有問題了麼?NO!!!!該方案帶來的結果同解一,原因也一樣,都將造成內存泄漏。此時“雙檢測鎖定”模式就粉墨登場了。

由Doug Schmidt和Tim Harrison提出了“雙檢測鎖定”(Double-Checked Locking)模式來解決multithread singletons問題。

Singleton& Singleton::Instance(){

If( !m_pInstance ){                                                      //1

Lock(m_mutex); //含義爲獲取互斥量 //2

If(!m_pInstance) //3

m_pInstance = new Singleton; //4

UnLock(m_mutex); //5

}

return *m_pInstance;                                                 //6

}

請看上面的第三句,這句話是不是具有化腐朽爲神奇的力量啊 ^_^

上面的方案就完美了麼。回答還是NO!!!(各位看官是否已經鬱悶了啊,這不是玩我啊?請耐心點,聽我細細到來^_^)

如果在RISC機器上編譯器有可能將上面的代碼優化,在鎖定m_mutex前執行第3句。這是完全有可能的,因爲第一句和第3句一樣,根據代碼優化原則是可以這樣處理的。這樣一來,我們引以爲自豪的“雙檢測鎖定”居然沒有起作用( L)

怎麼辦?解決唄。怎麼解決?簡單,我們在m_pInstance前面加一個修飾符就可以了。什麼修飾符呢?…….

 

àvolatile(簡單吧,讓我們J吧)

那麼我們完整的解法如下:

/*解三*/

class Singleton

{

public:

static Singleton &Instance(){                            //1

if( !m_pInstatnce){ //2

Lock(m_mutex) //3

If( !m_pInstance ) //4

m_pInstance = new Singleton;//5

UnLock(m_mutex); //6

}

return *m_pInstance; //7

}

private:

static volatitle Singleton *m_pInstatnce;            //8

private:

Singleton();                                                         //9

Singleton(const Singleton&);                             //10

Singleton& operator=(const Singleton&);            //11

~Singleton();                                                       //12

}

Singleton *Singleton:m_pInstatnce = NULL; //13

3         Singleton銷燬

在這裏,我們就到了Singleton最簡單也最複雜的地方了。

爲什麼說它簡單?我們根本可以不理睬創建的對象m_pInstance的銷燬啊。因爲雖然我們一直沒有將Singleton對象刪除,但不會造成內存泄漏。爲什麼這樣說呢?因爲只有當你分配了累積行數據並丟失了對他的所有reference是,內存泄漏才發生。而對Singleton並不屬於上面的情況,沒有累積性的東東,而且直到結束我們還有它的引用。在現代操作系統中,當一個進程結束後,將自動將該進程所有內存空間完全釋放。(可以參考《effective C++》條款10,裏面講述了內存泄漏)。

但有時泄漏還是存在的,那是什麼呢?就是資源泄漏。比如說如果該Singleton對象管理的是網絡連接,OS互斥量,進程通信的handles等等。這時我們就必須考慮到Singleton的銷燬了。談到銷燬,那可是一個複雜的課題(兩天三夜也說不完^_^  開玩笑的啦,大家輕鬆一下嘛)。

我們需要在恰當的地點,恰當的時機刪除Singleton對象,並且還要在恰當的時機創建或者重新創建Singleton對象。

在我們的“解二”中,在程序結束時會自動調用Singleton的析構函數,那麼也將自動釋放所獲取的資源。在大多數情況下,它都能夠有效運作。那特殊情況是什麼呢?

我們以KDL(keyboard,display,log)模型爲例,其中K,D,L均使用Singleton模式。只要keyboard或者display出現異常,我們就必須調用log將其寫入日誌中,否則log對象不應該創建。對後面一條,我們的Singleton創建時就可以滿足。

在前面我們已經說到,在產生一個對象時(非用new產生的對象),由編譯器自動調用了atexit(__DestroyObject)函數來實現該對象的析構操作。而C++對象析構是LIFO進行的,即先產生的對象後摧毀。

如果在一般情況下調用了log對象,然後開始銷燬對象。按照“後創建的先銷燬”原則:log對象將被銷燬,然後display對象開始銷燬。此時display在銷燬發現出現異常,於是調用log對象進行記錄。但事實上,log對象已經被銷燬,那麼調用log對象將產生不可預期的後果,此問題我們稱爲Dead Reference。所以前面的解決方案不能解決目前我們遇到的問題。

Andrei Alexandrescu提出瞭解決方案,稱爲Phoenix Singleton(取自鳳凰涅磐典故)

/*解四*/

class Singleton

{

public:

static Singleton &Instance(){                           

if( !m_pInstatnce){

Lock(m_mutex)

If( !m_pInstance ){

if(m_destroyed)

OnDeadReference();

else

Create();

}

UnLock(m_mutex);

}

return *m_pInstance;

}

private:

static volatitle Singleton *m_pInstatnce;

static bool m_destroyed;

private:

Singleton();                                                        

Singleton(const Singleton&);                            

Singleton& operator=(const Singleton&);    

~Singleton(){

m_pInstance = 0;

m_destroyed = true;

}

static void Create(){

static Singleton sInstance;

m_pInstanace = &sInstance;

}

static void OnDeadReference(){

Create();

new (m_pInstance) Singleton;

atexit(KillPhoenixSingleton);

m_destroyed = false;

}

void KillPhoenixSingleton(){

m_pInstance->~Singleton();

}

}

Singleton *Singleton:m_pInstatnce = NULL;

bool m_destroyed =false; 

請注意此處OnDeadReference()中所使用的new操作符的用法:是所謂的placement new操作,它並不分配內存,而是在某個地址上構造一個新對象。

這是解決Dead Reference方法之一。如果此時keyboard或者display對象也需要處理Dead Reference問題時,那麼上面的OnDeadReference將被頻繁調用,效率將會很低。即該問題爲:需要提供一種解決方案,用於處理對象的建立過程可以不按照“先創建會銷燬”的原則,而應該爲其指定一個銷燬順序。

聰明的Andrei Alexandrescu提出了一個“帶壽命的Singleton”解決方案。該方案的思想是:利用atexit()的特性;在每次創建一個對象後,將該對象放入到一個鏈表中(該鏈表是按照銷燬順序排訓的),並同時調用atexit()註冊一個銷燬函數;該銷燬函數從鏈表中獲取最需要銷燬的對象進行銷燬。(懶病又犯了L。該模式的實現請各位看官自行實現,可以參考《C++設計新思維》一書,Andrei Alexandrescu著)。

 

各位看官,看完本篇後,是否覺得Singleton還簡單啦J

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章