三種單例與線程安全單例模式詳解


//Singleton返回的實例的生存期是由Singleton本身所決定的,而不是用戶代碼。
//我們知道,指針和引用在語法上的最大區別就是指針可以爲NULL,並可以通過delete運算符刪除指針所指的實例,而引用則不可以。
//由該語法區別引申出的語義區別之一就是這些實例的生存期意 義:
//通過引用所返回的實例,生存期由非用戶代碼管理,而通過指針返回的實例,其可能在某個時間點沒有被創建,或是可以被刪除的。

//重用方法:class SingletonInstance : public Singleton<SingletonInstance>

//多線程懶漢式
template <typename T> 
class Singleton    
{     
public:    
	//static函數和變量 返回引用是重點
    static T& Instance()  
    {    
    	T* tmp = m_Instance;
        if(tmp == NULL)    
        {   //用lock實現線程安全      
            pthread_mutex_lock(mutex_); 
            tmp = m_Instance;           
            if(tmp == NULL)    
            {    
                tmp = new T();    
                m_Instance = tmp;
                atexit(&Singleton::Destroy);
            }   
            pthread_mutex_unlock(mutex_)  ; 
        }    
        return *m_Instance;    
    }  
private:
    //構造 拷貝 賦值均是私有或關閉   
    Singleton() {}    
    ~Singleton(){}
	Singleton(const Singleton&)=delete;  
    Singleton& operator=(const Singleton&)=delete;  

    static void Destroy()
    {
    	if(m_Instance != NULL)
    		delete m_Instance;
    	m_Instance = NULL;
    }
	static pthread_mutex_t mutex_;  
 	static T* m_Instance; 
 	//static volatile T* m_Instance; 
};    
template <typename T> T* volatile Singleton<T>::m_Instance = 0;      
template <typename T> pthread_mutex_t Singleton<T>::mutex_ = PTHREAD_MUTEX_INITIALIZER;
  
//單線程 懶漢式
template <typename T>     
class Singleton    
{    
private:    
    Singleton()  {}  
    ~Singleton() {}   
public:    
    Singleton(const Singleton&)=delete;    
    Singleton& operator=(const Singleton&)=delete;    
    //static函數和變量 返回引用是重點  
    static T& Instance()
    {  
        static T m_Instance;    
        return m_Instance;    
    }      
};


//餓漢式  
template <typename T> 
class Singleton {  
private:   
    static T* m_instance = new Singleton();  
	Singleton() {}
	~Singleton() {}
public:  
    Singleton(const Singleton&)=delete;  
    Singleton& operator=(const Singleton&)=delete;  
    T& Instance()   
    {  
        return *m_instance;  
    }  
};  

 

保護默認函數

Singleton限制其類型實例有且只能有一個,因此我們應通過將構造函數設置爲非公有來保證其不會被用戶代碼隨意創建,中間件代碼需要非常嚴謹才能防止用戶代碼的誤用。
在類型實例訪問函數中,我們通過局部靜態變量(懶漢)/成員靜態變量(飢漢)達到實例僅有一個的要求。
要保護的有構造函數,拷貝構造函數,析構函數以及賦值運算符。
Singleton所返回的常常是一個引用,對引用進行取址將得到相應類型的指針。而從語法上來說,引用和指針的最大區別在於是否可以被delete關鍵字刪除以及是否可以爲NULL。但是Singleton返回一個引用也 就表示其生存期由非用戶代碼所管理。因此使用取址運算符獲得指針後又用delete關鍵字刪除Singleton所返回的實例明顯是一個用戶錯誤。綜上所述,通過將取址運算符設置爲私有沒有多少意義。

飢漢與懶漢?生存期管理

對Singleton的生存期特性的討論需要分爲兩個方面:Singleton內使用的靜態變量的生存期以及 Singleton外在用戶代碼中所表現的生存期
懶漢Singleton內使用的靜態變量是一個局部靜態變量,因此只有在Singleton的 Instance()函數被調用時其纔會被創建,從而擁有了延遲初始化(Lazy)的效果,提高了程序的啓動性能。同時該實例將生存至程序執行完畢。而就 Singleton的用戶代碼而言,其生存期貫穿於整個程序生命週期,從程序啓動開始直到程序執行完畢。當然,懶漢Singleton在生存期上的一個缺陷就是創建和析構時的不確定性。由於Singleton實例會在Instance()函數被訪問時被創建,因此在某處新添加的一處對Singleton的訪問將可能導致Singleton的生存期發生變化。如果其依賴於其它組成,如另一個Singleton,那麼對它們的生存期進行管理將成爲一個災難。甚至可 以說,還不如不用Singleton,而使用明確的實例生存期管理。程序初始化及關閉時單件的構造及析構順序的不確定可能導致致命的錯誤
將Singleton的實現改爲使用全局靜態變量,稱爲餓漢Singleton,並將這些全局靜態變量在文件中按照特定順序排序,但是這樣的話,靜態變量將使用eager initialization的方式完成初始化,可能會對性能影響較大。但優點是線程安全性。對於具有關聯的兩個Singleton,對它們進行使用的代碼常常侷限在同一區域內。該問題的一個解決方法常常是將對它們進行使用的管理邏輯實現爲Singleton,而在內部邏輯中對它們進行明確的生存期管理。
全局靜態變量的生命週期:編譯器會在程序的main()函數執行之前插入一段代碼,用來初始化全局變量。當然,靜態變量也包含在內。該過程被稱爲靜態初始化。

多線程懶漢知識點

使用了一個指針記錄創建的Singleton實例. 爲了能滿足局部靜態變量只被初始化一次的需求,很多編譯器會通過一個全局的標誌位記錄該靜態變量是否已經被初始化的信息。那麼,對靜態變量進行初始化的僞碼就變成下面這個樣子:

1 bool flag = false;
2 if (!flag)
3 {
4     flag = true;
5     staticVar = initStatic();
6 }

在第一個線程執行完對flag的檢查並進入if分支後,第二個線程將可能被啓動,從而也進入if分支。這樣,兩個線程都將執行對靜態變量 的初始化。 因此在這裏,我使用了指針,並在對指針進行賦值之前使用鎖保證在同一時間內只能有一個線程對指針進行初始化。同時基於性能的考慮,我們需要在每次訪問實例之前檢查指針是否已經經過初始化,以避免每次對Singleton的訪問都需要請求對鎖的控制權。
同時因爲new運算符的調用分爲分配內存、調用構造函數以及爲指針賦值三步,就像下面的構造函數調用:

SingletonInstance pInstance = new SingletonInstance();

會轉化爲以下形式:

SingletonInstance pHeap = __new(sizeof(SingletonInstance));
pHeap->SingletonInstance::SingletonInstance();
SingletonInstance pInstance = pHeap;

這樣轉換是因爲在C++標準中規定,如果內存分配失敗,或者構造函數沒有成功執行, new運算符所返回的將是空。一般情況下,編譯器不會輕易調整這三步的執行順序,但是在滿足特定條件時,如構造函數不會拋出異常等,編譯器可能出於優化的 目的將第一步和第三步合併爲同一步,可以理解爲二三步互換:

SingletonInstance pInstance = __new(sizeof(SingletonInstance));
pInstance->SingletonInstance::SingletonInstance();

這樣就可能導致其中一個線程在完成了內存分配後就被切換到另一線程,而另一線程對Singleton的再次訪問將由於pInstance已經 賦值而越過if分支,從而返回一個不完整的對象。因此,我在這個實現中爲靜態成員指針添加了volatile關鍵字該關鍵字的實際意義是由其修飾的變量可能會被意想不到地改變,因此每次對其所修飾的變量進行操作都需要從內存中取得它的實際值。 它可以用來阻止編譯器對指令順序的調整。只是由於該關鍵字所提 、供的禁止重排代碼是假定在單線程環境下的,因此並不能禁止多線程環境下的指令重排。
或者用臨時指着T* tmp人工模擬將兩步擴展成三步。
最後來說說對atexit()關鍵字的使用。 在通過new關鍵字創建類型實例的時候,我們同時通過atexit()函數註冊了釋放該實例的函數,從而保證了這些實例能夠在 程序退出前正確順序地析構 。該函數的特性也能 保證後被創建的實例首先被析構 。 其實,對靜態類型實例進行析構的過程與前面所提到 的在main()函數執行之前插入靜態初始化邏輯相對應。

指針還是引用

因爲Singleton返回的實例的生存期是由Singleton本身所決定的,而不是用戶代碼。我們知道,指針和引用在語法上的最大區別就是指針可以爲NULL,並可以通過delete運算符刪除指針所指的實例而引用則不可以。由該語法區別引申出的語義區別之一就是這些實例的生存期意義:通過引用所返回的實例,生存期由非用戶代碼管理,而通過指針返回的實例,其可能在某個時間點沒有被創建,或是可以被刪除的。但是這兩條 Singleton都不滿足,因此在這裏,我使用指針,而不是引用。

指針與引用其他區別: 低層次向高層次上來說,分爲編譯器實現上的,語法上的以及語義上的區別。就編譯器的實現來說, 聲明一個引用並沒有爲引用分配內存,而僅僅是爲該變量賦予了一個別名。 而聲明一個指針則分配了內存。這種實現上的差異就導致了語法上的衆多區別:對引用進 行更改將導致其原本指向的實例被賦值,而對指針進行更改將導致其指向另一個實例;引用將永遠指向一個類型實例,從而導致其不能爲NULL ,並由於該限制而 導致了衆多語法上的區別,如 dynamic_cast對引用和指針在無法成功進行轉化時的行爲不一致。而就語義而言,前面所提到的生存期語義是一個區別, 同時一個返回引用的函數常常保證其返回結果有效。一般來說,語義區別的根源常常是語法上的區別,因此上面的語義區別僅僅是列舉了一些例子,而真正語義上的 差別常常需要考慮它們的語境。

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