天天看點

用C++實作單例模式1——懶漢模式和餓漢式

單例模式的定義:

    保證一個類僅有一個執行個體,并提供一個通路它的全局通路點,該執行個體被所有程式子產品共享。

    那麼我們就必須保證:

        1.該類不能被複制。

        2.該類不能被公開的創造。

    那麼對于C++來說,它的構造函數,拷貝構造函數和指派函數都不能被公開調用。

應用場景:

    比如在某個伺服器程式中,該伺服器的配置資訊存放在一個檔案中,這些配置資料由一個單例對象統一讀取,然後服務程序中的其他對象再通過這個單例對象擷取這些配置資訊。這種方式簡化了在複雜環境下的配置管理。其他還有如系統的日志輸出、MODEM的聯接需要一條且隻需要一條電話線,作業系統隻能有一個視窗管理器,一台PC連一個鍵盤等等。

根據單例對象建立時間,可分為兩種模式:餓漢模式 + 懶漢模式

1 、懶漢模式

        懶漢模式的特點是延遲加載,比如配置檔案,采用懶漢式的方法,顧名思義,懶漢麼,很懶的,配置檔案的執行個體直到用到的時候才會加載,不到萬不得已就不會去執行個體化類,也就是說在第一次用到類執行個體的時候才會去執行個體化。

        懶漢模式實作方式有兩種:

            (1)靜态指針 + 用到時初始化 

            (2) 局部靜态變量

        懶漢模式實作一:靜态指針 + 用到時初始化 

template<typename T>
class Singleton
{
public:
static T& getInstance()
{
    if (!value_)
    {
        value_ = new T();
    }
    return *value_;
}
private:
    Singleton();
    ~Singleton();
    static T* value_;
};
template<typename T>
T* Singleton<T>::value_ = NULL;
           

           在單線程中,這樣的寫法是可以正确使用的,但是在多線程中就不行了,該方法是線程不安全的。

            (1)假如線程A和線程B, 這兩個線程要通路getInstance函數,線程A進入getInstance函數,并檢測if條件,由于是第一次進入,value_為空,if條件成立,準備建立對象執行個體。

            (2)但是,線程A有可能被OS的排程器中斷而挂起睡眠,而将控制權交給線程B。

            (3)線程B同樣來到if條件,發現value_還是為NULL,因為線程A還沒來得及構造它就已經被中斷了。此時假設線程B完成了對象的建立,并順利的傳回。

            (4)之後線程A被喚醒,繼續執行new再次建立對象,這樣一來,兩個線程就建構兩個對象執行個體,這就破壞了唯一性。

         另外,還存在記憶體洩漏的問題,new出來的東西始終沒有釋放,下面是一種餓漢式的一種改進。

emplate<typename T>
class Singleton
{
public:
static T& getInstance()
{
    if (!value_)
    {
        value_ = new T();
    }
    return *value_;
}
private:
     class CGarbo     
    {    
    public:    
        ~CGarbo()    
        {    
            if(Singleton::value_)    
                delete Singleton::value_;    
        }    
    };    
    static CGarbo Garbo;    
    Singleton();
    ~Singleton();
    static T* value_;
};
template<typename T>
T* Singleton<T>::value_ = NULL;
           

  在程式運作結束時,系統會調用Singleton的靜态成員Garbo的析構函數,該析構函數會删除單例的唯一執行個體。使用這種方法釋放單例對象有以下特征:

  *在單例類内部定義專有的嵌套類。

  *在單例類内定義私有的專門用于釋放的靜态成員。

  *利用程式在結束時析構全局變量的特性,選擇最終的釋放時機

懶漢模式實作二: 局部靜态變量
template<typename T>
class Singleton
{
public:
static T& getInstance() 
{
    static T instance;
    return instance;
}
    
private:
    Singleton(){};
    Singleton(const Singleton&);
    Singleton& operator=(const Singleton&);
};
           

        同樣,靜态局部變量的實作方式也是線程不安全的。如果存在多個單例對象的析構順序有依賴時,可能會出現程式崩潰的危險。

        對于局部靜态對象的也是一樣的。因為 static T instance;語句不是一個原子操作,在第一次被調用時會調用Singleton的構造函數,而如果構造函數裡如果有多條初始化語句,則初始化動作可以分解為多步操作,就存在多線程競争的問題。

        為什麼存在多個單例對象的析構順序有依賴時,可能會出現程式崩潰的危險?原因:由于靜态成員是在第一次調用函數GetInstance時進行初始化,調用構造函數的,是以構造函數的調用順序時可以唯一确定了。對于析構函數,我們隻知道其調用順序和構造函數的調用順序相反,但是如果幾個Singleton類的析構函數之間也有依賴關系,而且出現類似單例執行個體A的析構函數中使用了單例執行個體B,但是程式析構時是先調用執行個體B的析構函數,此時在A析構函數中使用B時就可能會崩潰。

        在李書淦的部落格中給出了下面的例子。

#include <string>
#include <iostream>
using namespace std;
class Log
{
public:
    static Log* GetInstance()
    {
        static Log oLog;
        return &oLog;
    }
 
    void Output(string strLog)
    {
        cout<<strLog<<(*m_pInt)<<endl;
    }
private:
    Log():m_pInt(new int(3))
    {
    }
    ~Log()
    {cout<<"~Log"<<endl;
        delete m_pInt;
        m_pInt = NULL;
    }
    int* m_pInt;
};
 
class Context
{
public:
    static Context* GetInstance()
    {
        static Context oContext;
        return &oContext;
    }
    ~Context()
    {
        Log::GetInstance()->Output(__FUNCTION__);
    }
 
    void fun()
    {
        Log::GetInstance()->Output(__FUNCTION__);
    }
private:
    Context(){}
    Context(const Context& context);
};
 
int main(int argc, char* argv[])
{
    Context::GetInstance()->fun();
    return 0;
}
           

          在這個反例中有兩個Singleton: Log和Context,Context的fun和析構函數會調用Log來輸出一些資訊,結果程式Crash掉了,該程式的運作的序列圖如下(其中畫紅框的部分是出問題的部分):

用C++實作單例模式1——懶漢模式和餓漢式

       李書淦的部落格也給出了解決方案:對于析構的順序,我們可以用一個容器來管理它,根據單例之間的依賴關系釋放執行個體,對所有的執行個體的析構順序進行排序,之後調用各個單例執行個體的析構方法,如果出現了循環依賴關系,就給出異常,并輸出循環依賴環。具體分析見其部落格,連結在此:http://www.cnblogs.com/li_shugan/articles/1841382.html  。

      在masefee的部落格中也給出了多個單例模式在析構函數中互相引用時的解決方法,可以瞅瞅:  http://blog.csdn.net/masefee/article/details/5902266

2.餓漢式         餓了肯定要饑不擇食。是以在單例類定義的時候就進行執行個體化。因為main函數執行之前,全局作用域的類成員靜态變量m_Instance已經初始化,故沒有多線程的問題。

        實作方式也有兩種:

            (1)直接定義靜态對象

            (2)靜态指針 + 類外初始化時new空間實作

     餓漢式 實作一: 直接定義靜态對象

//.h檔案
class Singleton
{
public:
  static Singleton& GetInstance();
private:
  Singleton(){}
  Singleton(const Singleton&);
  Singleton& operator= (const Singleton&);
private:
  static Singleton m_Instance;
};
//CPP檔案
Singleton Singleton::m_Instance;//類外定義-不要忘記寫
Singleton& Singleton::GetInstance()
{
   return m_Instance;
}
//函數調用
Singleton& instance = Singleton::GetInstance();
           

     優點:          實作簡單,多線程安全。

    缺點:

      (1)如果存在多個單例對象且這幾個單例對象互相依賴,可能會出現程式崩潰的危險。原因:對編譯器來說,靜态成員變量的初始化順序和析構順序是一個未定義的行為;具體分析在懶漢模式中也講到了。

     (2)在程式開始時,就建立類的執行個體,如果Singleton對象産生很昂貴,而本身有很少使用,這種方式單從資源利用效率的角度來講,比懶漢式單例類稍差些。但從反應時間角度來講,則比懶漢式單例類稍好些。

   使用條件:

     (1)當肯定不會有構造和析構依賴關系的情況。

     (2)想避免頻繁加鎖時的性能消耗

餓漢式實作方式二:靜态指針 + 類外初始化時new空間實作

class Singleton
{
protected:
    Singleton(){}
private:
    static Singleton* p;
public:
    static Singleton* initance();
};
Singleton* Singleton::p = new Singleton;
Singleton* singleton::initance()
{
    return p;
}