單例模式
1.1定義以及作用
保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。保證一個類只有一個對象,降低對象之間的耦合度
1.2 特點
某個類只能有一個實例;其必須自行創建這個實例;必須自行向整個系統提供這個實例。
優點:
- 活動的單例只有一個實例,對單例類的所有實例化得到的都是相同的一個實例。這樣就防止了其他對象對自己就行實例化,確保每個對象都訪問一個實例
- 提供了對唯一實例的受控訪問
- 避免對共享資源的多重佔用
- 允許可變數目的實例
- 可節約系統資源,無須頻繁的創建和銷燬對象
缺點:
- 不適用變化的對象,若同一類型的對象總是要在不同的用例場景發生變化,則會一起數據的錯誤
- 由於沒有抽象層,因此可擴展難度大
- 其職責過重,在一定程度上違背了“單一職責原則”
1.3 UML類圖
1.4 單例模式說明
單例模式下又分爲懶漢模式和餓漢模式。
懶漢模式:顧名思義——懶,即只有在第一次用到類實例的時候纔會去實例化。(懶漢模式本身是線程不安全的)。特點 :在訪問量小時,採用懶漢模式,以時間換空間。
餓漢模式:單例類加載的時候就將自己以靜態初始化的方式實例化。特點:由於要進行線程同步,因此在訪問量比較大或可能訪問的線程比較多時,採用餓漢模式。以空間換時間。
適用場景:
- 需要頻繁實例化然後銷燬的對象
- 創建對象耗時過多或者耗資源過多,但又經常用到的對象
- 有狀態的工具類對象
- 頻繁訪問數據庫或文件的對象
- 其他只有一個對象的場景
1.5 代碼實現
測試平臺VS2015,單例模式的有以下幾種解法:
1.5.1 示例代碼一:單線程解法
缺點:多線程情況下,每個線程可能創建出不同的Singleton實例
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
#include <windows.h>
using namespace std;
#define MAX_Count 10
#define init_mode 1 //測試兩種不同的new實例化方式
//1--表示在類中new Singleton() 即懶漢模式
//0--表示在類外new Singleton()
class Singleton
{
public:
static Singleton* getInstance() {
#if init_mode == 1
if (m_pInstance == NULL)
{
Sleep(100); //放大多線程操作的效果
m_pInstance = new Singleton();
}
#else
Sleep(100); //放大多線程操作的效果 爲了與類中實例化等效 控制變量
#endif // init_mode == 1
return m_pInstance;
}
static void destoryInstance() {
if (m_pInstance != NULL)
{
delete m_pInstance;
m_pInstance = NULL;
}
}
private:
Singleton() {}
static Singleton* m_pInstance;
};
//Singleton實例初始化
#if init_mode == 1
//這裏線程是不安全的
Singleton* Singleton::m_pInstance = nullptr;
#else
//前面不能加static,會和類外全局static混淆
//在這裏實例化時 線程安全的 static初始化是在主函數main()之前
Singleton* Singleton::m_pInstance = new Singleton();
#endif // init_mode == 1
void print_singleton_instance() {
Singleton *sObj = Singleton::getInstance();
cout << sObj << endl;
}
//多線程獲取多次實例
void multithreading_test() {
// 預期結果:所有的實例指針指向的地址相同
vector<thread> threads;
for (int i = 0; i < MAX_Count; ++i) {
threads.push_back(thread(print_singleton_instance));
}
for (auto& t : threads) {
t.join();
}
#if init_mode == 1
Singleton::destoryInstance();
#endif // init_mode == 1
}
//單線程獲取多次實例
void singlethreading_test() {
// 預期結果:所有的實例指針指向的地址相同
vector<Singleton*> s;
for (int i = 0; i < MAX_Count; ++i) {
s.push_back(Singleton::getInstance());
cout << s[i] << endl;
}
//爲什麼0的時候不銷燬 由於我在main裏面多線程和單線程的一起測試
//若在0的銷燬,後面測試多線程的時候,地址爲空
#if init_mode == 1
Singleton::destoryInstance();
#endif // init_mode == 1
}
//test
int main() {
cout << "singlethreading_test : " << endl;
singlethreading_test();
cout << "multithreading_test : " << endl;
multithreading_test();
getchar();
return 0;
}
測試對比如下:
測試類型 | 類外初始化(init_mode=0 ) |
類中初始化(init_mode=1 ) |
---|---|---|
代碼測試結果 | ||
結果分析 | 單線程中的地址沒變,多線程地址也沒變 | 多線程測試下地址不一樣 |
小結:單線程解法在不同的實例化方法情況下,在多線程中每個線程可能創建出不同的Singleton實例
1.5.2 示例代碼二:多線程+加鎖
由於在示例一中,存在多線程情況下,可能創建多個實例的問題,因此改善其代碼,構造的時候加鎖。測試平臺依然是VS2015,修改部分的代碼如下:
class Singleton
{
public:
static Singleton* getInstance() {
#if init_mode == 1
//if (m_pInstance == NULL)
//{
// Sleep(100); //放大多線程操作的效果
// m_pInstance = new Singleton();
//}
if (m_pInstance == NULL)
{
m_mutex.lock(); //使用C++11中的多線程庫
if (m_pInstance == NULL)
{
Sleep(100); //放大多線程操作的效果
m_pInstance = new Singleton();
}
m_mutex.unlock();
}
#else
Sleep(100); //放大多線程操作的效果
#endif // init_mode == 1
return m_pInstance;
}
static void destoryInstance() {
if (m_pInstance != NULL)
{
delete m_pInstance;
m_pInstance = NULL;
}
}
private:
static mutex m_mutex; //定義互斥量
Singleton() {}
static Singleton* m_pInstance;
};
//Singleton實例初始化
//前面不能加static,會和類外全局static混淆
//在這裏實例化時 線程安全的 static初始化是在主函數main()之前
#if init_mode == 1
Singleton* Singleton::m_pInstance = nullptr;
mutex Singleton::m_mutex;
#else
Singleton* Singleton::m_pInstance = new Singleton();
#endif // init_mode == 1
這裏在new Singleton()
進行了加鎖操作,同時借鑑大話設計模式中的C#實現的單例模式,使用雙重鎖定(Double-Check Locking)。這樣做的目的是爲了當兩個線程同時調用getInstance()時,都可以通過第一次m_pInstance == NULL的判斷,然後由於Lock機制,這兩個線程則只有一個進入,另一個處於等候狀態。必須等待其中已經進入的線程出來之後,另一個處於等候的線程才能進入。而第二次判斷的這保證了當第一個線程創建了實例,第二個線程進來是不會繼續創建實例,這樣就達到了單例的目的
測試結果如下:
測試類型 | 雙重鎖定 | 不加第二次判斷 |
---|---|---|
測試結果 | ||
結果分析 | 多線程地址沒變 | 多線程測試下地址不一樣 |
1.5.3 示例代碼三:const static型實例
由於頻繁的進行加鎖和解鎖,可能造成資源浪費,因此加鎖操作將成爲一個性能的瓶頸,再次改進代碼,利用const static的特點:const只能定義一次,不能再次修改;static保持類內只有一個實例。代碼如下:
class Singleton
{
public:
static Singleton* getInstance() {
Sleep(100); //放大多線程操作的效果
return const_cast<Singleton *>(m_pInstance); // 去掉“const”特性;
}
static void destoryInstance() {
if (m_pInstance != NULL)
{
delete m_pInstance;
m_pInstance = NULL;
}
}
private:
Singleton() {}
static const Singleton* m_pInstance;
};
//利用const只能定義一次的特點
const Singleton* Singleton::m_pInstance = new Singleton();
void print_singleton_instance() {
Singleton *sObj = Singleton::getInstance();
cout << sObj << endl;
}
//多線程獲取多次實例
void multithreading_test() {
vector<thread> threads;
for (int i = 0; i < MAX_Count; ++i) {
threads.push_back(thread(print_singleton_instance));
}
for (auto& t : threads) {
t.join();
}
Singleton::destoryInstance();
}
//test
int main() {
cout << "multithreading_test : " << endl;
multithreading_test();
getchar();
return 0;
}
測試結果如下:
由於靜態初始化在程序開始時,即進入main函數之前,有主線程以單線程的方式完成了初始化,所以靜態初始化實例保證了線程安全。而且性能也得到提高,避免了頻繁的加鎖和解鎖操作。
1.5.4 示例代碼四:在get函數中創建並返回static臨時實例的引用
簡單粗暴的做法,既不加鎖也不使用const,但該方法不能認爲控制單例實例的銷燬。代碼如下:
class Singleton {
public:
static Singleton* getInstance() {
Sleep(100); //放大多線程操作的效果
static Singleton instance;
return &instance;
}
private:
Singleton() {}
};
void print_singleton_instance() {
Singleton *sObj = Singleton::getInstance();
cout << sObj << endl;
}
//多線程獲取多次實例
void multithreading_test() {
vector<thread> threads;
for (int i = 0; i < MAX_Count; ++i) {
threads.push_back(thread(print_singleton_instance));
}
for (auto& t : threads) {
t.join();
}
}
//test
int main() {
cout << "multithreading_test : " << endl;
multithreading_test();
getchar();
return 0;
}
測試結果如下:
1.5.5 示例代碼五:顯式控制實例銷燬
在上面的代碼示例中,除了示例四沒有使用new實例化對象,其他均使用了new操作符。雖然加入了destoryInstance(),但難免有時候會忘記調用該函數,,以及相關資源的釋放問題。在示例4的缺點是不能人爲控制實例的銷燬。結合上述兩種方法,修改代碼如下:
class Singleton {
public:
static Singleton* getInstance() {
Sleep(100); //放大多線程操作的效果
return instance;
}
private:
Singleton() {}
static Singleton* instance;
class GC //類似C#、Java的垃圾回收
{
public:
~GC() {
//可以在此處釋放所有需要釋放的資源,比如數據庫、文件等
if(instance != NULL)
{
cout << "GC will run" << endl;
delete instance;
instance = NULL;
cout << "GC completed " << endl;
getchar(); //增加演示效果
}
};
};
// 內部類的實例
static GC gc;
};
Singleton* Singleton::instance = new Singleton();
Singleton::GC Singleton::gc;
void print_singleton_instance() {
Singleton *sObj = Singleton::getInstance();
cout << sObj << endl;
}
//單線程獲取多次實例
void singlethreading_test() {
// 預期結果:所有的實例指針指向的地址相同
vector<Singleton*> s;
for (int i = 0; i < MAX_Count; ++i) {
s.push_back(Singleton::getInstance());
cout << s[i] << endl;
}
}
//多線程獲取多次實例
void multithreading_test() {
vector<thread> threads;
for (int i = 0; i < MAX_Count; ++i) {
threads.push_back(thread(print_singleton_instance));
}
for (auto& t : threads) {
t.join();
}
}
//test
int main() {
cout << "singlethreading_test : " << endl;
singlethreading_test();
cout << "multithreading_test : " << endl;
multithreading_test();
getchar();
return 0;
}
測試結果如下:
這裏GC will run前面的空格是爲了演示效果,main中的getchar()造成。可以看到當程序運行結束時,系統會調用Singleton的靜態成員GC的析構函數,進行資源的釋放,這樣無須人爲控制資源的釋放。其原理是:程序在結束時,系統會自動析構所有的全局變量,也會析構使用類的靜態成員變量,由於靜態變量和全局變量在內存中,均在靜態儲存區,在程序結束後,靜態存儲區的變量都會被釋放
1.5.6 示例代碼六:智能指針實現
程序員總是喜歡精益求精,不斷改進現有的代碼。上述代碼所解決的問題可看作指針創建以及銷燬。而C++11提供了智能指針這個東西,引用計數爲0的時候自動釋放內存,方便內存管理,因此嘗試用智能指針實現。示例代碼如下:
class Singleton {
public:
static shared_ptr<Singleton> getInstance() {
Sleep(100); //放大多線程操作的效果
return instance;
}
private:
Singleton() {}
Singleton(const Singleton &);
static shared_ptr<Singleton> instance;
~Singleton() {
cout << "單例對象銷燬!" << endl;
getchar();
};
};
shared_ptr<Singleton> Singleton::instance(new Singleton());
void print_singleton_instance() {
shared_ptr<Singleton> sObj = Singleton::getInstance();
cout << sObj << endl;
}
//單線程獲取多次實例
void singlethreading_test() {
// 預期結果:所有的實例指針指向的地址相同
vector<shared_ptr<Singleton>> s;
for (int i = 0; i < MAX_Count; ++i) {
s.push_back(Singleton::getInstance());
cout << s[i] << endl;
}
}
//多線程獲取多次實例
void multithreading_test() {
vector<thread> threads;
for (int i = 0; i < MAX_Count; ++i) {
threads.push_back(thread(print_singleton_instance));
}
for (auto& t : threads) {
t.join();
}
}
//test
int main() {
cout << "singlethreading_test : " << endl;
singlethreading_test();
cout << "multithreading_test : " << endl;
multithreading_test();
getchar();
return 0;
}
出現編譯錯誤:shared_ptr無法訪問private成員。這裏可以將析構函數變爲public,但是這就可能使用戶調用該函數,違背了封閉的原則,另外一種方法就是利用shared_ptr在定義的時候可以指定刪除器(deleter),參考別人的例子,定義一個static類型的銷燬函數,即可通過類名直接調用,示例代碼如下:
class Singleton {
public:
static shared_ptr<Singleton> getInstance() {
Sleep(100); //放大多線程操作的效果
return instance;
}
private:
Singleton() {}
Singleton(const Singleton &);
static shared_ptr<Singleton> instance;
~Singleton() {
cout << "單例對象銷燬!" << endl;
getchar();
};
//用來銷燬智能指針對象
static void Destory(Singleton *) {
cout << "在這裏銷燬單例對象!" << endl;
getchar();
};
};
shared_ptr<Singleton> Singleton::instance(new Singleton(),Singleton::Destory);
測試結果如下:
這裏可以看到銷燬智能指針實際上執行的是Destory函數並非~Singleton() ,刪除器聲明時(即static void Destory(Singleton *))
,需要傳入該對象的指針。原因查看出錯出的代碼得知。出錯處的代碼如下:
private:
template<class _Ux>
void _Resetp(_Ux *_Px)
{ // release, take ownership of _Px
_TRY_BEGIN // allocate control block and reset
_Resetp0(_Px, new _Ref_count<_Ux>(_Px));
_CATCH_ALL // allocation failed, delete resource
delete _Px;
_RERAISE;
_CATCH_END
}
template<class _Ux,
class _Dx>
void _Resetp(_Ux *_Px, _Dx _Dt)
{ // release, take ownership of _Px, deleter _Dt
_TRY_BEGIN // allocate control block and reset
_Resetp0(_Px, new _Ref_count_del<_Ux, _Dx>(_Px, _Dt));
_CATCH_ALL // allocation failed, delete resource
_Dt(_Px);
_RERAISE;
_CATCH_END
}
當我們利用前面出錯的代碼調用時,即未指定deleter,調用的是第一個_Resetp
;而指定指定deleter,調用的是第二個_Resetp
,此時deleter _Dt
需要傳入一個參數_Px
(即shared_ptr
內部對象的指針——Singleton *
),最後釋放shared_ptr
內部對象的內容。
1.6 小結
總結了單例模式的幾種實現方式,其中還有其他方式還未實現,如使用內存柵欄方式。同時複習和學習了C++11智能指針、線程、const和static等方面的知識。其中C++裏面的相關知識還需要深入學習理解,加深印象。