目錄
一、單例類簡介
單例模式就是讓整個程序中僅有該類的一個實例存在。
在很多情況下,只有一個實例是很重要的,比如一個打印機可以有很多打印任務,但是隻能有一個任務正在被執行;一個系統只能有一個窗口管理器和文件系統。
從具體實現上來講,單例類具有如下三個特點:1)構造函數、拷貝構造函數、操作符重載的賦值運算符函數應位於private權限修飾符下,若不如此的話,他人可以在類外調用這三個函數創建實例,那麼就無法實現單例模式。2)類的成員變量中包含一個該類的靜態私有對象,或一個指向該類對象的靜態私有指針。3)該類提供了一個靜態的公有函數,用於創建或獲取2)中的靜態私有對象或指針。
二、單例類的實現模式
單例類的實現模式有兩種:餓漢模式和懶漢模式。
餓漢模式:如同一個餓漢一樣,在進入main函數之前就創建好了實例,不管目前需不需要用到,這是一種空間換時間的做法。不需要進行線程保護。
懶漢模式:如同一個懶漢一樣,需要創建實例的時候纔去創建,否則就不創建,是一種時間換空間的做法。需要進行線程保護。
2.1餓漢模式代碼
餓漢模式中的單例類對象instance爲該單例類Singleton內的一個私有static成員(該成員也爲此單例類Singleton的對象),在加載類時便創建了,直到程序結束時才釋放,即餓漢模式下的單例類對象的生存週期與程序一樣長,因此,餓漢模式是線程安全的,因爲在線程創建之前,實例就已經被創建好了。
singleton.hpp
#pragma once
#include <iostream>
using namespace std;
class Singleton{
private:
Singleton(){
cout << "創建了一個單例對象" << endl;
}
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
~Singleton(){
//析構函數我們也需要聲明成private的
//因爲我們想要這個實例在程序運行的整個過程中都存在
//所以我們不允許實例自己主動調用析構函數釋放對象
cout << "銷燬了一個單例對象" << endl;
}
static Singleton instance; //這是我們的單例對象,注意這是一個類對象,下面會更改這個類型
public:
static Singleton* getInstance();
};
//下面這個靜態成員變量在類加載的時候就已經初始化好了
Singleton Singleton::instance;
Singleton* Singleton::getInstance(){
return &instance;
}
test.cpp
#include "singleton.hpp"
int main(){
cout << "Now we get the instance" << endl;
Singleton* instance1 = Singleton::getInstance();
Singleton* instance2 = Singleton::getInstance();
Singleton* instance3 = Singleton::getInstance();
cout<<"instance1:"<<instance1<<endl;
cout<<"instance2:"<<instance2<<endl;
cout<<"instance3:"<<instance3<<endl;
cout << "Now we destroy the instance" << endl;
return 0;;
}
最後的運行結果是:
由此可見,程序在進入main函數之前就已經創建好了一個單例對象instance,進入main函數後三次調用getInstance函數獲得的都是單例對象instance的地址,因此instance1、instance2和instance3中的值是一樣的,並沒有創建更多的實例。最後程序結束時調用析構函數銷燬了該單例對象。
2.2懶漢模式代碼
與餓漢模式不同,懶漢模式在需要時才創建實例,是一種以時間換空間的做法。在單線程情況下,它不需要進行線程保護,代碼如下。
#pragma once
#include <iostream>
using namespace std;
class Singleton{
private:
Singleton(){
cout << "創建了一個單例對象" << endl;
}
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
~Singleton(){
// 析構函數我們也需要聲明成private的
//因爲我們想要這個實例在程序運行的整個過程中都存在
//所以我們不允許實例自己主動調用析構函數釋放對象
cout << "銷燬了一個單例對象" << endl;
}
static Singleton* instance; //這是我們的單例對象,它是一個類對象的指針
public:
static Singleton* getInstance();
private:
//定義一個內部類
class Garbo{
public:
Garbo(){}
~Garbo(){
if(instance != NULL){
delete instance;
instance = NULL;
}
}
};
//定義一個內部類的靜態對象
//當該對象銷燬的時候,調用析構函數順便銷燬我們的單例對象
static Garbo _garbo;
};
//下面這個靜態成員變量在類加載的時候就已經初始化好了
Singleton* Singleton::instance = NULL;
Singleton::Garbo Singleton::_garbo; //還需要初始化一個垃圾清理的靜態成員變量
Singleton* Singleton::getInstance(){
if(instance == NULL)
instance = new Singleton();
return instance;
}
test.cpp
#include "singleton.hpp"
int main(){
cout << "Now we get the instance" << endl;
Singleton* instance1 = Singleton::getInstance();
Singleton* instance2 = Singleton::getInstance();
Singleton* instance3 = Singleton::getInstance();
cout<<"instance1:"<<instance1<<endl;
cout<<"instance2:"<<instance2<<endl;
cout<<"instance3:"<<instance3<<endl;
cout << "Now we destroy the instance" << endl;
return 0;
}
運行結果如下所示。
懶漢模式下的單例instance被初始化爲NULL,在調用getInstance函數獲取單例地址時,首先判斷instance是否爲NULL,如果爲NULL,代表單例還未被創建,使用new在堆上創建單例instance,並返回該instance的地址,下次再調用getInstance獲取單例地址時,因爲instance已經不爲NULL了,所以直接將上次在堆上創建的單例instance的地址返回。
另外,類中類Garbo是一個垃圾回收類,作用是在程序結束時釋放堆上的單例instance,原理是:在程序結束時會調用~Garbo()析構靜態成員變量_garbo,而在~Garbo()中,若判斷instance不爲空(即單例被創建了),則調用delete釋放堆上的單例,否則不做任何處理。
2.3多線程保護的懶漢模式
在上面的單線程情況下,懶漢模式是沒有問題的,可是在多線程情況下,它就需要進行線程保護!
爲什麼呢?假設線程1和線程2均調用了getInstance函數獲取單例。在線程1剛判斷完instance爲NULL,正準備調用new在堆上創建單例時,上下文切換到了線程2,此時線程2也認爲instance爲NULL,於是調用new創建了單例instance,然後又上下文切換到線程1,線程1接着之前的步驟執行,即調用new在堆上又創建了一個單例instance。大家肯定發現了吧,現在程序中存在着兩個Singleton對象instance,那麼Singleton對象還能被稱作爲單例類嗎?而這,就是BUG所在。
餓漢的單例模式在多線程情況下進行線程保護的代碼如下:
Singleton.hpp
#pragma once
#include <iostream>
using namespace std;
class Lock {
private:
mutex my_mutex;
public:
Lock(mutex my_mutex1) : my_mutex(my_mutex1){
m_cs.Lock();
}
~Lock(){
m_cs.Unlock();
}
};
class Singleton{
private:
Singleton(){
cout << "創建了一個單例對象" << endl;
}
Singleton(const Singleton&);
Singleton& operator=(const Singleton&);
~Singleton(){
// 析構函數我們也需要聲明成private的
//因爲我們想要這個實例在程序運行的整個過程中都存在
//所以我們不允許實例自己主動調用析構函數釋放對象
cout << "銷燬了一個單例對象" << endl;
}
static Singleton* instance; //這是我們的單例對象,它是一個類對象的指針
public:
static Singleton* getInstance();
static mutex my_mutex1;
private:
//定義一個內部類
class Garbo{
public:
Garbo(){}
~Garbo(){
if(instance != NULL){
delete instance;
instance = NULL;
}
}
};
//定義一個內部類的靜態對象
//當該對象銷燬的時候,調用析構函數順便銷燬我們的單例對象
static Garbo _garbo;
};
//下面這個靜態成員變量在類加載的時候就已經初始化好了
Singleton* Singleton::instance = NULL;
Singleton::Garbo Singleton::_garbo; //還需要初始化一個垃圾清理的靜態成員變量
Singleton* Singleton::getInstance(){
if(instance == NULL){
Lock lock(my_mutex1); // 加鎖
if(instance == NULL)
instance = new Singleton();
}
return instance;
}
其中,getInstance()中的第一個if(instance == NULL)是用來防止在線程很多的情況下,每個線程調用getInstance()時都進行加鎖解鎖,會浪費很多時間。第二個if(instance == NULL)則是用來判斷單例是否被創建了。另外,Lock類對象lock在構造時會執行my_mutex1.lock()加鎖,而在退出getInstance函數時,Lock類對象lock會自動調用析構函數~Lock(),從而執行my_mutex1.unlock()解鎖,實現了對單例類的多線程保護。
最後,推薦三篇比較經典的單例模式博文,同時也是本文所重點參考的。
1、https://blog.csdn.net/lvyibin890/article/details/81943637