設計模式(C++)——單例模式

單例模式

這是我平時用的比較多的模式。所謂單例模式,就是說我希望全局只有一個實例。比如說日誌模塊,整個系統我只希望有一個日誌模塊,每個模塊在打印日誌的時候我希望他們用到的都是同一個實例,而不是各自爲政,打印到了不同的日誌中。此時就需要用到單例模式。

單例模式在實現時,有懶漢模式與餓漢模式兩種方式。其中懶漢模式又分線程安全與不安全兩種。

1. 懶漢模式,線程不安全

懶漢模式,就是指你不調用接口,那我就永遠不真正生成實例,只有到真正用的時候纔會真正地生成實例。

class SingletonClass {
private:
    SingletonClass() { cout << "constructor" << endl; };
    ~SingletonClass() { cout << "destructor" << endl; }
public:
    SingletonClass(const SingletonClass&) = delete;
    SingletonClass& operator = (const SingletonClass&) = delete;
    
    static SingletonClass& getInstance() {
        static SingletonClass instance;
        return instance;
    }
};

這裏,我使用的是函數內static變量的形式,也可以使用類內static成員變量:

class SingletonClass {
private:
    static SingletonClass* instance;
    SingletonClass() { cout << "constructor" << endl; };
    ~SingletonClass() { cout << "destructor" << endl; }
public:
    SingletonClass(const SingletonClass&) = delete;
    SingletonClass& operator = (const SingletonClass&) = delete;
    
    static SingletonClass& getInstance() {
        if(nullptr == instance)
            instance = new SingletonClass;
        return *instance;
    }
    static void releaseOldInstance(){
        delete instance;
    }
};
SingletonClass* SingletonClass::instance = nullptr;

後面這個示例,我改用了指針來存儲實例,好處在於可以在適當的時候釋放單例模式的實例,而不必使其生命週期與整個程序一樣長;而且不必佔用棧空間。劣勢就在於,必須選擇正確的釋放時機(或者乾脆不釋放,但是我覺得沒有deletenew是不完整的)。

這裏注意:

兩個方式中getInstance()函數的返回值,我都選擇了引用的形式,而非網上很多例程中的指針形式。這是因爲我認爲指針形式在使用時會很讓人迷惑:調用者在使用完畢後到底要不要釋放指針呢? C語言沒有垃圾回收機制,因此在使用指針時必須異常謹慎。所以指針形式的返回值就會要求調用者在使用時需要知道更多的信息。同時,指針形式的返回值也會帶來潛在的威脅:如果調用者誤操作,將指針釋放了呢?而以引用形式返回,那麼除非調用者故意搞事情,否則很難造成威脅。(什麼?你問我他就是搞事情怎麼辦?這……那我還能怎麼辦?拖下去砍了啊!祭天!)

2. 懶漢模式,線程安全

class SingletonClass {
private:
    static SingletonClass* instance;
    static std::mutex lock;
    SingletonClass() { cout << "constructor" << endl; };
    ~SingletonClass() { cout << "destructor" << endl; }
public:
    SingletonClass(const SingletonClass&) = delete;
    SingletonClass& operator = (const SingletonClass&) = delete;
    
    static SingletonClass& getInstance() {
        if(nullptr == instance)
        {
            std::lock_guard<std::mutex> l(lock);
            if(nullptr == instance)
                instance = new SingletonClass;
        }
        return *instance;
    }
};
SingletonClass* SingletonClass::instance = nullptr;
std::mutex SingletonClass::lock;

這裏使用雙重檢測鎖機制來保證線程安全性。
其實這裏是一個簡化的、不完備的實現,因爲這裏我沒有寫releaseOldInstance接口,所以默認單例模式實例的生命週期與整個程序相同,因此永遠不釋放實例佔用的空間。如果想要添加釋放接口,那麼釋放時機也要選擇恰當,否則也不是線程安全的。而如果想保證恰當的釋放時機,我們可能需要用到一個新的類以及C++14中的讀寫鎖纔可以:

template<class T>
class SingletonInstancePtr;

class SingletonClass {
private:
    int x = 0;
    SingletonClass() { cout << "constructor" << endl; };
    ~SingletonClass() { cout << "destructor" << endl; }
protected:
    static SingletonClass* instance;
    static std::shared_mutex lock;
    static SingletonClass* getInstance() {
        if (nullptr == instance)
        {
            std::lock_guard<std::shared_mutex> l(lock);
            if (nullptr == instance)
                instance = new SingletonClass();
        }
        return instance;
    }
    friend class SingletonInstancePtr<SingletonClass>;
public:
    SingletonClass(const SingletonClass&) = delete;
    SingletonClass& operator = (const SingletonClass&) = delete;

    static void releaseOldInstance()
    {
        if (nullptr != instance)
        {
            std::lock_guard<std::shared_mutex> l(lock);
            if (nullptr != instance)
            {
                delete instance;
                instance = nullptr;
            }
        }
    }
    int getX() { return x; }
    int addX() { return ++x; }
};
SingletonClass* SingletonClass::instance = nullptr;
std::shared_mutex SingletonClass::lock;

template<class T>
class SingletonInstancePtr
{
private:
    T* instance = nullptr;
    std::shared_lock<std::shared_mutex> lock;
public:
    SingletonInstancePtr()
    {
        do {
            instance = T::getInstance();
            auto tmp = std::shared_lock<std::shared_mutex>(T::lock);
            if (T::instance != nullptr)
            {
                lock = std::move(tmp);
                break;
            }
        } while (true);
    }
    T* operator -> ()
    {
        return instance;
    }
};

int main()
{
    {
        SingletonInstancePtr<SingletonClass> instance;
        cout << instance->getX() << endl;
        cout << instance->addX() << endl;
    }
    SingletonClass::releaseOldInstance();
}

這裏我用了一個模板類,以便於如果有多個單例模式,可以複用這個模板。
SingletonInstancePtr模板類與讀寫鎖保證了,當有人在使用SingletonClass時,不能釋放相應的資源。
當然這裏還可以繼續改進。這裏沒有設置獲取鎖等待的時間,可能會造成程序鎖死。爲了保證不卡死,可以設置等待時間,或者嘗試次數等機制。

3. 餓漢模式

餓漢模式,就是在程序加載的時候,就直接初始化好,自然也就不存在線程安全不安全的問題了。

class SingletonClass {
private:
    static SingletonClass instance;
    SingletonClass() { cout << "constructor" << endl; };
    ~SingletonClass() { cout << "destructor" << endl; }

public:
    SingletonClass(const SingletonClass&) = delete;
    SingletonClass& operator = (const SingletonClass&) = delete;

    static SingletonClass& getInstance() {
        return instance;
    }
};

SingletonClass SingletonClass::instance;

這裏我還是用的非指針形式。如果類佔用內存過大,或者對於棧空間有要求,那麼也可以像之前一樣使用指針的形式,只要在初始化時,不要將instance指針初始化爲nullptr,而是直接new一個實例即可。

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