劍指offer筆記面試題2----實現Singleton模式

題目:設計一個類,咱們只能生成該類的一個實例。

解法一:單線程解法

//缺點:多線程狀況下,每一個線程可能建立出不一樣的的Singleton實例
    #include <iostream>
    using namespace std;
    
    class Singleton
    {
    public:
        static Singleton* getInstance()
        {
            if(m_pInstance == nullptr)
            {
                m_pInstance = new Singleton();
            }
            return m_pInstance;
        }
    
        static void destroyInstance()
        {
            if(m_pInstance != nullptr)
            {
                delete m_pInstance;
                m_pInstance = nullptr;
            }    
        }
    private:
        Singleton(){}
        static Singleton* m_pInstance;
    };
    
    Singleton* Singleton::m_pInstance = nullptr;
    
    // 單線程獲取屢次實例
    void Test1(){
        // 預期結果:兩個實例指針指向的地址相同
        Singleton* singletonObj = Singleton::getInstance();
        cout << singletonObj << endl;
        Singleton* singletonObj2 = Singleton::getInstance();
        cout << singletonObj2 << endl;
        Singleton::destroyInstance();
    }
    
    int main(){
        Test1();
        return 0;
    }

解法二:多線程+加鎖

/*解法一是最簡單,也是最廣泛的實現方式。可是,這種實現方式有不少問題,好比沒有考慮多線程的問題,在多線程的狀況下,就可能會建立多個Singleton實例,如下是改善的版本。*/
    #include <iostream>
    #include <mutex>
    #include <thread>
    #include <vector>
    using namespace std;
    
    class Singleton
    {
    private:
        static mutex m_mutex; // 互斥量
    
        Singleton(){}
        static Singleton* m_pInstance;
    
    public:
        static Singleton* getInstance(){
            if(m_pInstance == nullptr){
                m_mutex.lock(); // 使用C++11中的多線程庫
                if(m_pInstance == nullptr){ // 兩次判斷是否爲NULL的雙重檢查
                    m_pInstance = new Singleton();
                }
                m_mutex.unlock();
            }
            return m_pInstance;
        }
    
        static void destroyInstance(){
            if(m_pInstance != nullptr){
                delete m_pInstance;
                m_pInstance = nullptr;
            }
        }
    };
    
    Singleton* Singleton::m_pInstance = nullptr;
    mutex Singleton::m_mutex;
    
    
    void print_singleton_instance(){
        Singleton *singletonObj = Singleton::getInstance();
        cout << singletonObj << endl;
    }
    
    // 多個進程得到單例
    void Test1(){
        // 預期結果,打印出相同的地址,之間可能缺失換行符,也屬正常現象
        vector<thread> threads;
        for(int i = 0; i < 10; ++i){
            threads.push_back(thread(print_singleton_instance));
        }
    
        for(auto& thr : threads){
            thr.join();
        }
    }
    
    int main(){
        Test1();
        Singleton::destroyInstance();
        return 0;
    }
    /*此方法中進行了兩次m_pInstance == nullptr的判斷,使用了所謂的「雙檢鎖」機制。由於進行一次加鎖和解鎖是須要付出對應的代價的,而進行兩次判斷,就能夠避免屢次加鎖與解鎖操做,只在m_pInstance不爲nullptr時才須要加鎖,同時也保證了線程安全。可是,若是進行大數據的操做,加鎖操做將成爲一個性能的瓶頸,爲此,一種新的單例模式的實現也就出現了。*/

解法三:const static型實例

#include <iostream>
    #include <thread>
    #include <vector>
    using namespace std;
    
    class Singleton
    {
    private:
        Singleton(){}
        static const Singleton* m_pInstance;
    public:
        static Singleton* getInstance(){
            return const_cast<Singleton*>(m_pInstance); // 去掉「const」特性
            // 注意!若該函數的返回值改成const static型,則此處沒必要進行const_cast靜態轉換
            // 因此該函數能夠改成:
            /*
            const static Singleton* getInstance(){
                return m_pInstance;
            }
            */
        }
    
        static void destroyInstance(){
            if(m_pInstance != NULL){
                delete m_pInstance;
                m_pInstance = NULL;
            }
        }
    };
    const Singleton* Singleton::m_pInstance = new Singleton(); // 利用const只能定義一次,不能再次修改的特性,static繼續保持類內只有一個實例
    
    void print_singleton_instance(){
        Singleton *singletonObj = Singleton::getInstance();
        cout << singletonObj << endl;
    }
    
    // 多個進程得到單例
    void Test1(){
        // 預期結果,打印出相同的地址,之間可能缺失換行符,也屬正常現象
        vector<thread> threads;
        for(int i = 0; i < 10; ++i){
            threads.push_back(thread(print_singleton_instance));
        }
        for(auto& thr : threads){
            thr.join();
        }
    }
    
    int main(){
        Test1();
        Singleton::destroyInstance();
        return 0;
    }
    /*由於靜態初始化在程序開始時,也就是進入主函數以前,由主線程以單線程方式完成了初始化,因此靜態初始化實例保證了線程安全性。在性能要求比較高時,就可使用這種方式,從而避免頻繁的加鎖和解鎖形成的資源浪費。因爲上述三種實現,都要考慮到實例的銷燬,關於實例的銷燬,待會在分析。*

解法四:在get函數中建立並返回static臨時實例的引用

//PS:該方法不能認爲控制單例實例的銷燬
    #include <iostream>
    #include <thread>
    #include <vector>
    using namespace std;
    
    class Singleton
    {
    private:
        Singleton(){}
    
    public:
        static Singleton* getInstance(){
            static Singleton m_pInstance; // 注意,聲明在該函數內
            return &m_pInstance;
        }
    };
    
    void print_singleton_instance(){
        Singleton *singletonObj = Singleton::getInstance();
        cout << singletonObj << endl;
    }
    
    // 多個進程得到單例
    void Test1(){
        // 預期結果,打印出相同的地址,之間可能缺失換行符,也屬正常現象
        vector<thread> threads;
        for(int i = 0; i < 10; ++i){
            threads.push_back(thread(print_singleton_instance));
        }
    
        for(auto& thr : threads){
            thr.join();
        }
    }
    
    // 單個進程得到屢次實例
    void Test2(){
        // 預期結果,打印出相同的地址,之間換行符分隔
        print_singleton_instance();
        print_singleton_instance();
    }
    
    int main(){
        cout << "Test1 begins: " << endl;
        Test1();
        cout << "Test2 begins: " << endl;
        Test2();
        return 0;
    }

解法五:最終方案,最簡&顯式控制實例銷燬

/*在實際項目中,特別是客戶端開發,實際上是不在意這個實例的銷燬的。由於,全局就這麼一個變量,全局都要用,它的生命週期伴隨着軟件的生命週期,軟件結束了,他就天然而然結束了,由於一個程序關閉以後,它會釋放它佔用的內存資源的,因此,也就沒有所謂的內存泄漏了。
    可是,有如下狀況,是必需要進行實例銷燬的:
        在類中,有一些文件鎖了,文件句柄,數據庫鏈接等等,這些隨着程序的關閉而不會當即關閉的資源,必需要在程序關閉前,進行手動釋放。*/
    #include <iostream>
    #include <thread>
    #include <vector>
    using namespace std;
    
    class Singleton
    {
    private:
        Singleton(){}
        static Singleton* m_pInstance;
    
        // **重點在這**
        class GC // 相似Java的垃圾回收器
        {
        public:
            ~GC(){
                // 能夠在這裏釋放全部想要釋放的資源,好比數據庫鏈接,文件句柄……等等。
                if(m_pInstance != NULL){
                    cout << "GC: will delete resource !" << endl;
                    delete m_pInstance;
                    m_pInstance = NULL;
                }
            };
        };
    
        // 內部類的實例
        static GC gc;
    
    public:
        static Singleton* getInstance(){
            return m_pInstance;
        }
    };
    
    
    Singleton* Singleton::m_pInstance = new Singleton();
    Singleton::GC Singleton::gc;
    
    void print_instance(){
        Singleton* obj1 = Singleton::getInstance();
        cout << obj1 << endl;
    }
    
    // 多線程獲取單例
    void Test1(){
        // 預期輸出:相同的地址,中間可能缺失換行符,屬於正常現象
        vector<thread> threads;
        for(int i = 0; i < 10; ++i){
            threads.push_back(thread(print_instance));
        }
    
        for(auto& thr : threads){
            thr.join();
        }
    }
    
    // 單線程獲取單例
    void Test2(){
        // 預期輸出:相同的地址,換行符分隔
        print_instance();
        print_instance();
        print_instance();
        print_instance();
        print_instance();
    }
    
    int main()
    {
        cout << "Test1 begins: " << endl;
        cout << "預期輸出:相同的地址,中間能夠缺失換行(每次運行結果的排列格式一般不同)。" << endl;
        Test1();
        cout << "Test2 begins: " << endl;
        cout << "預期輸出:相同的地址,每行一個。" << endl;
        Test2();
        return 0;
    }
    /*在程序運行結束時,系統會調用Singleton的靜態成員GC的析構函數,該析構函數會進行資源的釋放,而這種資源的釋放方式是在程序員「不知道」的狀況下進行的,而程序員不用特別的去關心,使用單例模式的代碼時,沒必要關心資源的釋放。
    那麼這種實現方式的原理是什麼呢?因爲程序在結束的時候,系統會自動析構全部的全局變量,系統也會析構全部類的靜態成員變量,由於靜態變量和全局變量在內存中,都是存儲在靜態存儲區的,全部靜態存儲區的變量都會被釋放。因爲此處是用了一個內部GC類,而該類的做用就是用來釋放資源。這種技巧在C++中是普遍存在的,參見《C++中的RAII機制》。*/
相關文章
相關標籤/搜索