<!-- @page { margin: 2cm } H1 { margin-left: 0.16cm; margin-right: 0.16cm; margin-top: 0.16cm; margin-bottom: 0.16cm; background: #ffffff; color: #000000; background: #ffffff } H1.western { font-family: "Times New Roman"; font-size: 18pt } H1.cjk { font-family: "Times New Roman"; font-size: 16pt; font-style: normal; font-weight: bold } H1.ctl { font-family: "Times New Roman"; font-size: 16pt; font-weight: bold } P { margin-bottom: 0.21cm } A:link { so-language: zxx } -->
Singleton非常的有用,但通常也是煩惱之源。本文包括了個人實踐中遇到的各種各樣的Singleton,同時包括了C++和Java部分。
關於什麼是Singleton的思考。
Singleton模式通常用於只需要一個對象生存的場合,但是這句話不是Singleton的全部意義,模式不是公式,它是可以變化的。比如:一個系統打印對象,進程內只需要一個,但是一個多線程並發訪問數據庫的程序,每個線程可能都需要一個連接對象,這樣Singleton就意味著進程內有多個,每個線程裡面有一個。但是如果允許連接多個數據庫呢?很有可能就變成了每個線程內只允許有一個連著某個特定數據庫的連接對象。
Singleton的實現者需要提供一個全局的訪問點給用戶。最簡單的就是靜態成員函數Instance,為什麼不用全局變量,因為全局變量不能阻止別人創建同類型的變量,而且也污染了全局空間(別人不能和你用一樣的變量明)。所以我們要把類的構造函數變為protected。一個對象內部可以保存一個靜態指針,然後在Instance函數內部實例化,並返回它。這種解決方案可以解決剛才打印機的要求。一個對象可以保存一個靜態map,map每一項保存線程ID和靜態對象指針,並且提供維護該map的一系列方法,這樣就可以解決每個線程需要有一個對象的要求。但是這樣就夠了麼,我還碰到一個不同的需求,要求運行時決定創建不同的對象。這時候可以在Instance函數上加上參數,通過參數來創建不同的對象。這些對象也可以派生自父Singleton類。也許每個線程允許對象數目不能超過3個,沒關系,我們可以在Instance內部實現這些控制邏輯。
我想表達的是,Singleton可以有很多變種,有時候甚至讓你感覺名不副實,但是這就是設計模式的魅力。我也是從一開始的教條主義到能接受很多精彩的變化,有時候甚至都不應該用它,或許很多是多態就夠了
C++部分
http://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf 這是一篇Scott Meyers and Andrei Alexandrescu的論文。
1)不要用靜態或者全局變量來實現Singleton
由於C++不能保證靜態或者全局對象的構造函數的調用順序以及析構順序。所以如果程序中有多個用此方法實現的Singleton類,它們之間又有某種構造依賴關系和析構依賴關系,就會造成災難性的後果。所以,只有當肯定不會有構造和析構依賴關系的情況下,這種實現才是合適的。不過,對於C++,采用另一種靜態對象的方式更加簡單和方便。因此這種方式基本上可以放棄了。
2)Meyers Singleton來控制構造順序,但是不能控制析構順序
Scott Meyer在<<Effective C++>>3rd Item4中提出了一個解決方案,當將non-local static變量移動到靜態方法中成為local static變量的時候。C++保證當第一次靜態方法被調用的時候,才會創建該靜態變量。但是這裡有一個疑問,創建順序能夠被控制了,可是析構順序呢?我們只知道進程結束的時候,local static 變量會被析構,而且按照創建順序的相反順序進行。如果幾個Singleton類的析構函數之間也有依賴關系,並且這種依賴順序關系和LIFO順序沖突,就會造成dead-reference問題。
3)Andrei在<<Modern C++ Designe>>第6章中提到的解決方案思路簡單描述如下:
a.用new來分配Singleton對象,
b.每個Singleton對象都有一個整形壽命計數器,值大者壽命長
c.用一個特殊設計的數組來保存需要被銷毀的Singleton對象的指針。壽命越長的總是在數組前面,壽命相同的按照創建順序由前到後排列。
d.在std::at_exit中注冊一個清理函數,該函數總是從c描述的數組中取出最後一個指針,然後調用delete。
4)支持多線程。能夠控制構造和析構的順序之後,現在考慮多線程。一般采用Double-Checked Locking模式。第一次check不用加鎖,但是第二次check和創建對象必須加鎖。還要注意編譯器可能會優化代碼,導致Double-Checked Locking模式失效。因此要使用volatile 修飾T* pInstance變量。
5)Loki最後提出了一個基於策略的SingletonHolder類,完美的解決了以上問題。注意SingletonHolder只支持一般意義的單例,也就是進程內唯一對象。SingletonHodler提供了更多的策略類來滿足不同的要求。具體可以參考Loki文檔或者。SingletonHolder接收三個策略類,分別用於管理創建和銷毀對象,生命周期和線程策略。具體使用例子可以參考書本。
6)ACE和boost都提供了各自的解決方案。相比而言,SingletonHolder更靈活,能夠面對各種情況。
SingletonHolder例子:
下載最新版源代碼,在自己的C++程序中設置好include目錄,並將singleton.cpp加入到makefile中。
#include <cstdlib>
#include "loki/Singleton.h"
#include <iostream>
using namespace std;
class MyClass{
public:
void ShowPtr()
{
cout << this << endl;
}
};
unsigned int GetLongevity(MyClass*){
return 1;
}
/*
*
*/
int main(int argc, char** argv) {
MyClass c=Loki::SingletonHolder<MyClass,Loki::CreateUsingNew,Loki::SingletonWithLongevity>::Instance();//單線程環境下,用new和delete創建和銷毀對象,用 GetLongevity定義壽命。
c.ShowPtr();
return 0;
}
如果要支持多線程,應該在自己的應用程序中定義這個宏:LOKI_CLASS_LEVEL_THREADING。使用下面的代碼創建:
MyClass c=Loki::SingletonHolder<
MyClass,
Loki::CreateUsingNew,
Loki::SingletonWithLongevity,
Loki::ClassLevelLockable,
Loki::Mutex
>::Instance();
既然是多線程環境下的Singleton,那麼就應該使用Mutext進行同步,並且使用volatile強制從內存中讀取數據。
SingletonHolder只支持標准Singleton,即進程內唯一。不支持變種的Singleton,比如每個線程只有一個對象,或者其他的情況,需要我們自己設計。
Java部分
1.public static final 成員變量,同時將構造函數變為private.客戶程序直接訪問成員變量即可。
2.private static final 成員變量。然後提供工程方法getInstance(),返回靜態成員變量.由於工廠方法帶來的靈活性,我們可以講進程內唯一對象,變為線程內唯一對象。在修改getInstance方法的實現代碼時不會影響到客戶代碼的調用。
3.Single類最好不要實現Serializable接口。因為每次反序列化生成的對象都是一個新的對象,打破了Singleton原則。作為補救措施,可以通過將所有成員變量聲明為transient,並提供一個readResolve方法。所有成員變量都不參與序列化和反序列化的過程,並且readResolve總是返回一個Singleton對象。實際上這些動作也就表明不再支持序列化。因此最佳選擇是不要實現Serializable接口。
4.單元素枚舉類型。使用方法類似1,很簡單。但是同時也實現了Serializable接口,並且也能自動防止反序列化生成新對象。如果不考慮線程,這是比前面三中都好的方式。
// Enum singleton - the preferred approach
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() { ... }
}
5.延遲創建(按需創建或稱懶漢模式)
Google公司的工程師Bob Lee寫的新的懶漢單例模式
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton(){
}
public Singleton getInstance(){
return SingletonHolder.INSTANCE;
}
}
在加載singleton時並不加載它的內部類SingletonHolder,而在調用getInstance()時調用SingletonHolder時才加載SingletonHolder,從而調用singleton的構造函數,實例化singleton,從而達到lazy loading的效果。
6.Double-checked Locking模式在Java中是不能用的。原因有兩個:
一是Java編譯器處於優化的原因,生成的代碼會不按照我們編寫的順序。極有可能先給instance變量賦值,然後再構造對象。這樣一來在多線程環境下基於instance==null做判斷就會得到錯誤的結果。
二是Java synchrorized保證了被保護的變量總是從內存中讀取數據,而不是使用寄存器緩存的數據。但是當第一個線程已經創建好了對象,其他線程再來訪問的時候,由於沒有進入synchronized保護的代碼,因此沒有重新讀取內存中的數據,因此有可能獲取的是舊版本的數據。
詳細理由參考下面的文章以及其參考的其他文章:
html">http://dev.firnow.com/course/3_program/java/javashl/2008414/110150.html