文章目錄
1. 單例模式
1.1 定義
單例模式:保證一個類僅有一個實例,並提供一個訪問他的全局訪問點。[DP]
通常我們可以定義一個全局變量使得對象被訪問,但它不能防止你實例化多個對象。一個最好的方法就是,讓類自身負責保存它的唯一實例。這個類可以保證沒有其他實例可以被創建,並且它可以提供一個訪問該類實例的方法。
1.2 單例模式結構圖
1.3 分類
單例模式的實現分爲懶漢式和餓漢式:
- 懶漢式:在類實例第一次被使用時纔會將自己實例化。
- 餓漢式:在類被加載時就將自己實例化。
1.3 運用場景
運用場景舉例如下:
- 設備管理器,系統中可能有多個設備,但是隻有一個設備管理器,用於管理設備驅動;
- 數據池,用來緩存數據的數據結構,需要在一處寫,多處讀取或者多處寫,多處讀取。
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++ 單例模式總結與剖析