一、什么是單例模式
屬于創(chuàng)建型模式,它提供了一種創(chuàng)建對(duì)象的最佳方式。這種模式涉及到一個(gè)單一的類,該類負(fù)責(zé)創(chuàng)建自己的對(duì)象,同時(shí)確保只有單個(gè)對(duì)象被創(chuàng)建。這個(gè)類提供了一種訪問(wèn)其唯一的對(duì)象的方式,可以直接訪問(wèn),不需要實(shí)例化該類的對(duì)象。
注意:
- 單例類只能有一個(gè)實(shí)例。
- 單例類必須自己創(chuàng)建自己的唯一實(shí)例。
- 單例類必須給所有其他對(duì)象提供這一實(shí)例。
二、適用場(chǎng)景和實(shí)現(xiàn)要點(diǎn)
1、適用場(chǎng)景
- 需要生成唯一序列的環(huán)境(頻繁訪問(wèn)數(shù)據(jù)庫(kù)或文件)。
- 需要頻繁實(shí)例化然后銷毀的對(duì)象。
- 創(chuàng)建對(duì)象時(shí)耗時(shí)過(guò)多或者耗資源過(guò)多,但又經(jīng)常用到的對(duì)象。
- 方便資源相互通信的環(huán)境
1.1 單例模式經(jīng)典使用場(chǎng)景
- 資源共享的情況下,避免由于資源操作時(shí)導(dǎo)致的性能或損耗等。如上述中的日志文件,應(yīng)用配置。
- 控制資源的情況下,方便資源之間的互相通信。如線程池等。
1.2 應(yīng)用場(chǎng)景舉例
- 外部資源:每臺(tái)計(jì)算機(jī)有若干個(gè)打印機(jī),但只能有一個(gè)PrinterSpooler,以避免兩個(gè)打印作業(yè)同時(shí)輸出到打印機(jī)。內(nèi)部資源:大多數(shù)軟件都有一個(gè)(或多個(gè))屬性文件存放系統(tǒng)配置,這樣的系統(tǒng)應(yīng)該有一個(gè)對(duì)象管理這些屬性文件 。
- Windows的Task Manager(任務(wù)管理器)就是很典型的單例模式。
- windows的Recycle Bin(回收站)也是典型的單例應(yīng)用。在整個(gè)系統(tǒng)運(yùn)行過(guò)程中,回收站一直維護(hù)著僅有的一個(gè)實(shí)例。
- 網(wǎng)站的計(jì)數(shù)器,一般也是采用單例模式實(shí)現(xiàn),否則難以同步。
- 應(yīng)用程序的日志應(yīng)用,一般都何用單例模式實(shí)現(xiàn),這一般是由于共享的日志文件一直處于打開(kāi)狀態(tài),因?yàn)橹荒苡幸粋€(gè)實(shí)例去操作,否則內(nèi)容不好追加。
- Web應(yīng)用的配置對(duì)象的讀取,一般也應(yīng)用單例模式,這個(gè)是由于配置文件是共享的資源。
- 數(shù)據(jù)庫(kù)連接池的設(shè)計(jì)一般也是采用單例模式,因?yàn)閿?shù)據(jù)庫(kù)連接是一種數(shù)據(jù)庫(kù)資源。數(shù)據(jù)庫(kù)軟件系統(tǒng)中使用數(shù)據(jù)庫(kù)連接池,主要是節(jié)省打開(kāi)或者關(guān)閉數(shù)據(jù)庫(kù)連接所引起的效率損耗,這種效率上的損耗還是非常昂貴的,因?yàn)楹斡脝卫J絹?lái)維護(hù),就可以大大降低這種損耗。
- 多線程的線程池的設(shè)計(jì)一般也是采用單例模式,這是由于線程池要方便對(duì)池中的線程進(jìn)行控制。
- 操作系統(tǒng)的文件系統(tǒng),也是大的單例模式實(shí)現(xiàn)的具體例子,一個(gè)操作系統(tǒng)只能有一個(gè)文件系統(tǒng)。
- HttpApplication 也是單位例的典型應(yīng)用。熟悉ASP.Net(IIS)的整個(gè)請(qǐng)求生命周期的人應(yīng)該知道HttpApplication也是單例模式,所有的HttpModule都共享一個(gè)HttpApplication實(shí)例.
2、實(shí)現(xiàn)要點(diǎn)
- 將該類的
,這樣其他處的代碼就無(wú)法通過(guò)調(diào)用該類的構(gòu)造方法來(lái)實(shí)例化該類的對(duì)象,只有通過(guò)該類提供的靜態(tài)方法來(lái)得到該類的唯一實(shí)例;
-
;
- 在該類內(nèi)提供一個(gè)
,當(dāng)我們調(diào)用這個(gè)方法時(shí),如果類持有的引用不為空就返回這個(gè)引用,如果類保持的引用為空就創(chuàng)建該類的實(shí)例并將實(shí)例的引用賦予該類保持的引用;
- 提供一個(gè)靜態(tài)的公有的函數(shù)用于創(chuàng)建或獲取它本身的靜態(tài)私有對(duì)象。
三、C++實(shí)現(xiàn)單例的幾種方式
1、有缺陷的懶漢式
懶漢式(Lazy-Initialization)的方法是,也就說(shuō)直到調(diào)用getInstance() 方法的時(shí)候才 new 一個(gè)單例的對(duì)象, 如果不被調(diào)用就不會(huì)占用內(nèi)存。
// 有如下問(wèn)題:
// 1.線程不安全
// 2.內(nèi)存泄漏
class Singleton
{
private:
Singleton()
{
std::cout<<"創(chuàng)建構(gòu)造函數(shù)!"<<std::endl;
}
Singleton(Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
static Singleton* m_pInstance;
public:
~Singleton()
{
std::cout<<"析構(gòu)函數(shù)!"<<std::endl;
}
static Singleton* getInstance()
{
if (m_pInstance == nullptr)
{
m_pInstance = new Singleton;
}
return m_pInstance ;
}
};
Singleton* Singleton::m_instance_ptr = nullptr;
int main()
{
Singleton* instance = Singleton::getInstance();
Singleton* instance_2 = Singleton::getInstance();
return 0;
}
運(yùn)行結(jié)果是
創(chuàng)建構(gòu)造函數(shù)!
可以看到,獲取了兩次類的實(shí)例,卻只有一次類的構(gòu)造函數(shù)被調(diào)用,表明只生成了唯一實(shí)例,這是個(gè)最基礎(chǔ)版本的單例實(shí)現(xiàn),同時(shí)也存在一些問(wèn)題
- 線程安全問(wèn)題,當(dāng)多線程獲取單例時(shí)有可能引發(fā)競(jìng)態(tài)條件:
第一個(gè)線程在if中判斷m_pInstance 為空,實(shí)例化單例對(duì)象;同時(shí)第二個(gè)線程在獲取單例時(shí)也判斷m_pInstance 為空,實(shí)例化單例對(duì)象,會(huì)實(shí)例化兩個(gè)對(duì)象。- 內(nèi)存泄漏,注意到類中只負(fù)責(zé)new出對(duì)象,卻沒(méi)有負(fù)責(zé)delete對(duì)象,因此只有構(gòu)造函數(shù)被調(diào)用,析構(gòu)函數(shù)卻沒(méi)有被調(diào)用;因此會(huì)導(dǎo)致內(nèi)存泄漏。
2、線程安全且內(nèi)存安全的懶漢式單例 (智能指針,鎖)
class Singleton
{
public:
typedef std::shared_ptr<Singleton> Ptr;
~Singleton()
{
std::cout<<"析構(gòu)函數(shù)!"<<std::endl;
}
Singleton(Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
static Ptr getInstance()
{
// 雙檢鎖
if (m_pInstance == nullptr)
{
std::lock_guard<std::mutex> lock(m_mutex);
if (m_pInstance == nullptr)
{
m_pInstance = std::shared_ptr<Singleton>(new Singleton);
}
}
return m_pInstance ;
}
private:
Singleton()
{
std::cout<<"創(chuàng)建構(gòu)造函數(shù)!"<<std::endl;
}
static Ptr m_pInstance;
static std::mutex m_mutex;
};
Singleton::Ptr Singleton::m_instance_ptr = nullptr;
std::mutex Singleton::m_mutex;
int main()
{
Singleton* instance = Singleton::getInstance();
Singleton* instance_2 = Singleton::getInstance();
return 0;
}
運(yùn)行結(jié)果如下,發(fā)現(xiàn)確實(shí)只構(gòu)造了一次實(shí)例,并且發(fā)生了析構(gòu)。
創(chuàng)建構(gòu)造函數(shù)!
析構(gòu)函數(shù)!
shared_ptr和mutex都是C++11的標(biāo)準(zhǔn),以上這種方法的優(yōu)點(diǎn)是
- 基于 shared_ptr, 用了C++比較倡導(dǎo)的 RAII思想,用對(duì)象管理資源,當(dāng) shared_ptr 析構(gòu)的時(shí)候,new 出來(lái)的對(duì)象也會(huì)被 delete掉。以此避免內(nèi)存泄漏。
- 加了鎖,使用互斥量來(lái)達(dá)到線程安全。這里使用了兩個(gè) if判斷語(yǔ)句的技術(shù)稱為雙檢鎖;好處是,只有判斷指針為空的時(shí)候才加鎖,避免每次調(diào)用 getInstance的方法都加鎖,鎖的開(kāi)銷畢竟還是有點(diǎn)大的。
不足之處在于: 使用智能指針會(huì)要求用戶也得使用智能指針,非必要不應(yīng)該提出這種約束; 使用鎖也有開(kāi)銷; 同時(shí)代碼量也增多了,實(shí)現(xiàn)上我們希望越簡(jiǎn)單越好。還有更為嚴(yán)重的問(wèn)題,在某些平臺(tái)(與編譯器和指令集架構(gòu)有關(guān)),雙檢鎖會(huì)失效!
3、最推薦的懶漢式單例(magic static )——局部靜態(tài)變量
class Singleton
{
public:
~Singleton()
{
std::cout<<"析構(gòu)函數(shù)!"<<std::endl;
}
Singleton(Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
static Singleton& getInstance()
{
static Singleton instance;
return instance;
}
private:
Singleton()
{
std::cout<<"創(chuàng)建構(gòu)造函數(shù)!"<<std::endl;
}
};
int main()
{
Singleton& instance = Singleton::getInstance();
Singleton& instance_2 = Singleton::getInstance();
return 0;
}
運(yùn)行結(jié)果
創(chuàng)建構(gòu)造函數(shù)!
析構(gòu)函數(shù)!
運(yùn)用C++靜態(tài)變量的特性,生存周期是從聲明到程序結(jié)束。
四、單例模式優(yōu)缺點(diǎn)
優(yōu)點(diǎn)
- 在單例模式中,活動(dòng)的單例只有一個(gè)實(shí)例,對(duì)單例類的所有實(shí)例化得到的都是相同的一個(gè)實(shí)例。這樣就防止其它對(duì)象對(duì)自己的實(shí)例化,確保所有的對(duì)象都訪問(wèn)一個(gè)實(shí)例
- 單例模式具有一定的伸縮性,類自己來(lái)控制實(shí)例化進(jìn)程,類就在改變實(shí)例化進(jìn)程上有相應(yīng)的伸縮性。
- 提供了對(duì)唯一實(shí)例的受控訪問(wèn)。
- 由于在系統(tǒng)內(nèi)存中只存在一個(gè)對(duì)象,因此可以 節(jié)約系統(tǒng)資源,當(dāng) 需要頻繁創(chuàng)建和銷毀的對(duì)象時(shí)單例模式無(wú)疑可以提高系統(tǒng)的性能。
- 允許可變數(shù)目的實(shí)例。
- 避免對(duì)共享資源的多重占用。
缺點(diǎn)
- 不適用于變化的對(duì)象,如果同一類型的對(duì)象總是要在不同的用例場(chǎng)景發(fā)生變化,單例就會(huì)引起數(shù)據(jù)的錯(cuò)誤,不能保存彼此的狀態(tài)。
- 由于單利模式中沒(méi)有抽象層,因此單例類的擴(kuò)展有很大的困難。
- 單例類的職責(zé)過(guò)重,在一定程度上違背了“單一職責(zé)原則”。
- 濫用單例將帶來(lái)一些負(fù)面問(wèn)題,如為了節(jié)省資源將數(shù)據(jù)庫(kù)連接池對(duì)象設(shè)計(jì)為的單例類,可能會(huì)導(dǎo)致共享連接池對(duì)象的程序過(guò)多而出現(xiàn)連接池溢出;如果實(shí)例化的對(duì)象長(zhǎng)時(shí)間不被利用,系統(tǒng)會(huì)認(rèn)為是垃圾而被回收,這將導(dǎo)致對(duì)象狀態(tài)的丟失。