設計模式——單例模式(Singleton)

1. 單例模式

1.1 定義

單例模式:保證一個類僅有一個實例,並提供一個訪問他的全局訪問點。[DP]

通常我們可以定義一個全局變量使得對象被訪問,但它不能防止你實例化多個對象。一個最好的方法就是,讓類自身負責保存它的唯一實例。這個類可以保證沒有其他實例可以被創建,並且它可以提供一個訪問該類實例的方法。

1.2 單例模式結構圖

在這裏插入圖片描述

1.3 分類

單例模式的實現分爲懶漢式和餓漢式:

  • 懶漢式:在類實例第一次被使用時纔會將自己實例化。
  • 餓漢式:在類被加載時就將自己實例化。

1.3 運用場景

運用場景舉例如下:

  1. 設備管理器,系統中可能有多個設備,但是隻有一個設備管理器,用於管理設備驅動;
  2. 數據池,用來緩存數據的數據結構,需要在一處寫,多處讀取或者多處寫,多處讀取。

2. 單例模式的實現

2.1 C++實現

2.1.1 基礎要點

  • 全局只有一個實例。
  • 線程安全
  • 禁止賦值和拷貝
  • 用戶通過接口獲取實例

2.1.2 懶漢式

class Singleton
{
private:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
public:
    static Singleton* GetInstance()
    {
        if (m_pInstance == nullptr)
        {
            m_pInstance = new Singleton;
        }
        return m_pInstance;
    }
private:
    static Singleton* m_pInstance;
};

Singleton* Singleton::m_pInstance = nullptr;

上述懶漢式模式存在的問題:

  • 線程安全問題:上述代碼在單線程下工作是沒有問題的,但是,在多線程時獲取單例對象可能引發競爭條件:第一個線程判斷m_pInstance爲空,則開始實例化對象,與此同時,第二個線程也獲取單例對象,判斷m_pInstance也爲空,則實例出多個單例對象。
  • 內存泄漏:由於單例對象由類自身進行管理,外部不會delete m_pInstance。導致只new,沒有delete。另一種情況,若多線程中,同時new了多個對象,最終m_pInstance指向其中一個,導致其他對象指針成爲野指針。

針對上述問題,第一條我們可以通過加鎖解決,第二條我們可以通過使用智能指針來解決。因此有了以下實現。

#include <memory>
#include <mutex>

class Singleton
{
private:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
public:
    static std::shared_ptr<Singleton> GetInstance()
    {
    	//雙檢鎖,第一次判斷防止每次進入函數都加鎖帶來的性能開銷
    	//第二次判斷,防止多線程程序多次創建實例
        if (m_pInstance == nullptr)
        {
            std::lock_guard<std::mutex> lock(m_mutex);
            if (m_pInstance == nullptr)
            {
                m_pInstance = std::make_shared<Singleton>();
            }
        }
        return m_pInstance;
    }
private:
    static std::shared_ptr<Singleton> m_pInstance;
    static std::mutex m_mutex;
};

std::shared_ptr<Singleton> Singleton::m_pInstance = nullptr;
std::mutex Singleton::m_mutex;

上述方式也還是有問題的,使用了智能指針要求用戶也使用智能指針,這可以通過從智能指針中獲取原始指針並返回原始指針來解決,但是,必須保證用戶在外部不要調用delete,且不再用智能指針二次包裝原始指針,否則也會引發問題。
另外,在某些平臺(與編譯器和指令集架構有關),雙檢鎖會失效!
針對上述問題,結合c++11特性,有了以下實現。

class Singleton
{
private:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
public:
    static Singleton& GetInstance()
    {
    	//c++11 Magic Static特性:如果當變量在初始化的時候,
    	//併發同時進入聲明語句,併發線程將會阻塞等待初始化結束。
    	//Meyers' SingletonMeyer's的單例,
    	//是著名的寫出《Effective C++》系列書籍的作者 Meyers 提出的.
        static Singleton instance;
        return instance;
    }
};

上述代碼一次解決了多線程問題,內存安全問題,並且利用C++靜態變量的生存期是從聲明開始到程序結束的特性。該方式是最推薦方式。

2.1.3 餓漢式

餓漢式的實現相對簡單,代碼如下:

class Singleton
{
private:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
public:
    static Singleton& GetInstance()
    {
        return m_instance;
    }
private:
    static Singleton m_instance;
};

Singleton Singleton::m_instance;

上述實現方式在類加載時初始化實例對象,並直到程序結束,不管用戶需要還是不需要該單例,稱爲餓漢式單例,增加了內存佔用,是一種以空間換時間的方式。

2.1.4 基於CRTP(奇異的遞歸模板模式)的單例模式

template<typename T>
class Singleton
{
protected:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
public:
    virtual ~Singleton() {}
    static T& GetInstance()
    {
        static T instance;
        return instance;
    }
};

class DerivedSingle :public Singleton<DerivedSingle>
{
    friend class Singleton<DerivedSingle>;
private:
    DerivedSingle() {}
    DerivedSingle(const DerivedSingle&) = delete;
    DerivedSingle& operator = (const DerivedSingle&) = delete;
};

int main(int argc, char* argv[]) 
{
    DerivedSingle& instance1 = DerivedSingle::GetInstance();
    DerivedSingle& instance2 = DerivedSingle::GetInstance();
    return 0;
}

上述代碼需要在子類中將基類聲明爲友元,這樣才能調用子類的私有構造函數,使用起來較爲不便。在 stackoverflow上, 有大神給出了不需要在子類中聲明友元的方法,在這裏一併放出;精髓在於使用一個代理類 token,子類構造函數需要傳遞token類才能構造,但是把 token保護其起來, 然後子類的構造函數就可以是公有的了,這個子類只有 Derived(token)的這樣的構造函數,這樣用戶就無法自己定義一個類的實例了,起到控制其唯一性的作用。改進如下:

template<typename T>
class Singleton
{
protected:
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
public:
    virtual ~Singleton() {}
    static T& GetInstance()
    {
        static T instance{ token() };
        return instance;
    }
protected:
    struct token {};
};

class DerivedSingle :public Singleton<DerivedSingle>
{
    friend class Singleton<DerivedSingle>;
public:
    DerivedSingle(token) {}
    DerivedSingle(const DerivedSingle&) = delete;
    DerivedSingle& operator = (const DerivedSingle&) = delete;
};

int main(int argc, char* argv[]) 
{
    DerivedSingle& instance1 = DerivedSingle::GetInstance();
    DerivedSingle& instance2 = DerivedSingle::GetInstance();
    return 0;
}

3. 致謝

本文主要參考來源C++ 單例模式總結與剖析

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