C++ 單例模式的幾種實現研究


都是從網上學得,整理下自己的理解。 

 

單例模式有兩種實現模式:

1)懶漢模式: 就是說當你第一次使用時才創建一個唯一的實例對象,從而實現延遲加載的效果。

2)餓漢模式: 就是說不管你將來用不用,程序啟動時就創建一個唯一的實例對象。

 

所以,從實現手法上看,  懶漢模式是在第一次使用單例對象時才完成初始化工作。因為此時可能存在多線程競態環境,如不加鎖限制會導致重復構造或構造不完全問題。

餓漢模式則是利用外部變量,在進入程序入口函數之前就完成單例對象的初始化工作,此時是單線程所以不會存在多線程的競態環境,故而無需加鎖。 

 

以下是典型的幾種實現

一、 懶漢模式,標准的 ”雙檢鎖“  + ”自動回收“ 實現

class Singleton
{
public:
    static Singleton* GetInstance()
    {
        if (m_pInstance == NULL )
        {
            Lock(); // 加鎖
            if (m_pInstance == NULL )
            {
                m_pInstance = new Singleton ();
            }
            UnLock(); // 解鎖
        }
        return m_pInstance;
    }

    // 實現一個內嵌垃圾回收類    
    class CGarbo 
    {
    public:
        ~CGarbo()
        {
            if(Singleton::m_pInstance) 
                delete Singleton::m_pInstance;
        }
    };

    static CGarbo Garbo; // 定義一個靜態成員變量,程序結束時,系統會自動調用它的析構函數從而釋放單例對象

private:
    Singleton(){};
    Singleton(Singleton const&); 
    Singleton& operator=(Singleton const&); 

    static Singleton* m_pInstance;
};

Singleton* Singleton::m_pInstance = NULL;
Singleton::CGarbo Garbo;

 

 

二、靜態局部變量的懶漢模式 ,而不是new在堆上創建對象,避免自己回收資源。

這里仍然要注意的是局部變量初始化的線程安全性問題,在C++0X以后,要求編譯器保證靜態變量初始化的線程安全性,可以不加鎖。但C++ 0X以前,仍需要加鎖。

class Singleton
{
public:
    static Singleton* GetInstance()
    {
        Lock(); // not needed after C++0x 
        static Singleton instance;  
        UnLock(); // not needed after C++0x 

        return &instance;
    }

private:
    Singleton() {};
    Singleton(const Singleton &);
    Singleton & operator = (const Singleton &);
};

在懶漢模式里,如果大量並發線程獲取單例對象,在進行頻繁加鎖解鎖操作時,必然導致效率低下。

 

三、餓漢模式,基礎版本

因為程序一開始就完成了單例對象的初始化,所以后續不再需要考慮多線程安全性問題,就可以避免懶漢模式里頻繁加鎖解鎖帶來的開銷。

class Singleton
{
public:

    static Singleton* GetInstance()
    {
        return &m_instance;
    }

private:
    Singleton(){};
    Singleton(Singleton const&); 
    Singleton& operator=(Singleton const&); 

    static Singleton m_instance;
};

Singleton Singleton::m_instance;  // 在程序入口之前就完成單例對象的初始化

雖然這種實現在一定程度下能良好工作,但是在某些情況下會帶來問題 --- 就是在C++中 ”非局部靜態對象“ 的 ”初始化“ 順序 的 ”不確定性“, 參見Effective c++ 條款47。

考慮: 如果有兩個這樣的單例類,將分別生成單例對象A, 單例對象B. 它們分別定義在不同的編譯單元(cpp中), 而A的初始化依賴於B 【 即A的構造函數中要調用B::GetInstance() ,而此時B::m_instance 可能還未初始化,顯然調用結果就是非法的 】, 所以說只有B在A之前完成初始化程序才能正確運行,而這種跨編譯單元的初始化順序編譯器是無法保證的。 

 

四、餓漢模式,增強版本(boost實現)

在前面的方案中:餓漢模式中,使用到了類靜態成員變量,但是遇到了初始化順序的問題; 懶漢模式中,使用到了靜態局部變量,但是存在着線程安全等問題。

boost 的實現方式是:單例對象作為靜態局部變量,然后增加一個輔助類,並聲明一個該輔助類的類靜態成員變量,在該輔助類的構造函數中,初始化單例對象。以下為代碼

class Singleton
{
public:
    static Singleton* GetInstance()
    {
        static Singleton instance;
        return &instance;
    }

protected:
    // 輔助代理類
    struct Object_Creator
    {
        Object_Creator()
        {
            Singleton::GetInstance();
        }
    };
    static Object_Creator _object_creator;

    Singleton() {}
    ~Singleton() {}
};

Singleton::Object_Creator Singleton::_object_creator; 

首先,代理類這個外部變量初始化時,在其構造函數內部調用 Singleton::GetInstance();從而間接完成單例對象的初始化,這就通過該代理類實現了餓漢模式的特性。

其次,仍然考慮第三種模式的缺陷。 當A的初始化依賴於B, 【 即A的構造函數中要調用B::GetInstance() ,而此時B::m_instance 可能還未初始化,顯然調用結果就是非法的 】 現在就變為【在A的構造函數中要調用B::GetInstance() ,如果B尚未初始化,就會引發B的初始化】,所以在不同編譯單元內全局變量的初始化順序不定的問題就隨之解決。  

 

 

 

最后,關於使用懶漢還是餓漢模式,我的理解:

如果這個單例對象構造十分耗時或者占用很多資源,比如加載插件啊, 初始化網絡連接啊,讀取文件啊等等,而有可能該對象程序運行時不會用到,那么也要在程序一開始就進行初始化,也是一種資源浪費吧。 所以這種情況懶漢模式(延遲加載)更好。

如果這個單例對象在多線程高並發環境下頻繁使用,性能要求較高,那么顯然使用餓漢模式來避免資源競爭,提高響應速度更好。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM