“對象性能”模式
面向?qū)ο蠛芎玫慕鉀Q了“抽象”的問題碑诉,但是必不可免地要付出一定的代價彪腔。對于通常情況來講,面向?qū)ο蟮某杀敬蠖伎梢院雎圆挥嫿浴5悄承┣闆r德挣,面向?qū)ο笏鶐淼某杀颈仨氈?jǐn)慎處理。
- 典型模式
- Sington
- Flyweight
單例模式Singleton
保證一個類僅有一個實(shí)例快毛,并提供一個該實(shí)例的全局訪問點(diǎn)格嗅。
——《設(shè)計模式》GoF
- 動機(jī)
在軟件系統(tǒng)中,經(jīng)常有這樣一個特殊的類祸泪,必須保證它們在系統(tǒng)中只存在一個示例吗浩,才能確保他們的邏輯正確性、以及良好的效率没隘。
這個應(yīng)該類設(shè)計者的責(zé)任懂扼,而不是使用者的責(zé)任。
單例模式的代碼:
class Singleton{
private:
Singleton();
Singleton(const Singleton& other);
public:
static Singleton* getInstance();
static Singleton* m_instance;
};
Singleton* Singleton::m_instance=nullptr;
//線程非安全版本
Singleton* Singleton::getInstance() {
if (m_instance == nullptr) {
m_instance = new Singleton();
}
return m_instance;
}
/*
在單線程環(huán)境下右蒲,以上的代碼沒問題阀湿,但是在多線程的情況下會出問題磨德。
當(dāng)線程1執(zhí)行到 if (m_instance == nullptr) 時最疆,如果這時候正好線程2獲得了CPU的執(zhí)行權(quán),
那么摔竿,此時對于兩個線程來說间坐,都檢測到了這個對象為空灾挨,
那么兩者都會創(chuàng)建該對象,也就是會破壞了單例的本質(zhì)
*/
/*
為了解決以上多線程的問題竹宋,就出現(xiàn)了下面的線程安全的版本劳澄,通過鎖對象的方案來解決。
也就是說在一個線程執(zhí)行到getInstance方法時蜈七,在鎖對象未被釋放前秒拔,不會交出CPU的執(zhí)行權(quán)。
那么此時可以解決好多線程問題飒硅,但是另外一個問題同時產(chǎn)生砂缩,
那就是這樣的代碼,效率相對比較低三娩,破壞了多線程機(jī)制庵芭。
如果在代碼部署在服務(wù)器端,在對象創(chuàng)建的開始時雀监,如果有兩個客戶端訪問双吆,
那么一個進(jìn)入了鎖對象,那么他必然會獲得鎖對象,
而另一個只有等待第一個用戶完成后才能進(jìn)入getIntances方法來獲取對象伊诵。
并且對于對象創(chuàng)建完成之后单绑,所有的getInstance方法來說,
都是讀取這個進(jìn)程曹宴,
但每次都會有一個鎖對象搂橙。那么資源是浪費(fèi)的。如果高并發(fā)的情況笛坦,也會拖累效率区转。
*/
//線程安全版本,但鎖的代價過高
Singleton* Singleton::getInstance() {
Lock lock;
if (m_instance == nullptr) {
m_instance = new Singleton();
}
return m_instance;
}
/*
那么版扩,為了解決以上的問題废离,如果為空的情況,
也就是創(chuàng)建的時候才去創(chuàng)建鎖對象
通過這樣的方法可以避免在讀取的時候每次都創(chuàng)建鎖對象礁芦。
但是在這個代碼中蜻韭,必須要對所創(chuàng)建的對象判空兩次。
因?yàn)槿绻慌幸淮慰帐量郏€是會出現(xiàn)線程安全的問題肖方。
*/
//雙檢查鎖,但由于內(nèi)存讀寫reorder不安全
Singleton* Singleton::getInstance() {
if(m_instance==nullptr){
Lock lock;
if (m_instance == nullptr) {
m_instance = new Singleton();
}
}
return m_instance;
}
/*
對于雙檢查看起來已經(jīng)很好的完成了Singleton的要求和線程安全的問題未状。但實(shí)際上很容易出問題俯画。
但是以上的代碼實(shí)際存在漏洞,雙檢查在內(nèi)存讀寫時會出現(xiàn)reorder不安全的情況司草。
reorder:我們看代碼有一個指令序列艰垂,但代碼在匯編之后,可能在執(zhí)行的時候埋虹,搶CPU的指向權(quán)的時候猜憎,可能和我們預(yù)想的不一樣。
一般m_instance = new Singleton();只想的時候我們認(rèn)為是先分配內(nèi)存吨岭,再調(diào)用構(gòu)造函數(shù)創(chuàng)建對象拉宗,再把對象的地址賦值給變量峦树。
但在CPU實(shí)際執(zhí)行的時候辣辫,以上的三個步驟可能會被重新打亂順序執(zhí)行。
可能會是先分配內(nèi)存魁巩,然后就把內(nèi)存地址直接賦值給變量急灭,最后在調(diào)用構(gòu)造函數(shù)來創(chuàng)建對象。
那么如果出現(xiàn)以上的reorder的情況谷遂,變量已經(jīng)被賦值了對象的指針葬馋,但實(shí)際卻指向了沒被初始化的內(nèi)存。
那么此時,線程安全問題就再次出現(xiàn)了畴嘶。
*/
/*
在java和C#這類語言來說蛋逾,增加了一個volatile關(guān)鍵字,通過他來修飾單例的對象窗悯,此時編譯器不會在進(jìn)行reorder的優(yōu)化編譯区匣,以此保證代理的正確性。
2005年VC的編譯器自己添加了volatile關(guān)鍵字蒋院,但跨平臺的問題沒辦法解決亏钩。直到C++11后才真正的解決了這個問題,實(shí)現(xiàn)了跨平臺欺旧。
具體代碼如下:
*/
//C++ 11版本之后的跨平臺實(shí)現(xiàn) (volatile)
std::atomic<Singleton*> Singleton::m_instance; //首先聲明了一個原子的對象姑丑。
std::mutex Singleton::m_mutex;
Singleton* Singleton::getInstance() {
Singleton* tmp = m_instance.load(std::memory_order_relaxed);//通過原子的對象的load方法獲得對象的指針。
std::atomic_thread_fence(std::memory_order_acquire);//獲取內(nèi)存fence
//此時編譯不會被reorder
if (tmp == nullptr) {
std::lock_guard<std::mutex> lock(m_mutex);
tmp = m_instance.load(std::memory_order_relaxed);
if (tmp == nullptr) {
tmp = new Singleton;
std::atomic_thread_fence(std::memory_order_release);//釋放內(nèi)存fence
m_instance.store(tmp, std::memory_order_relaxed);
}
}
return tmp;
}
要點(diǎn)總結(jié)
- Singleton模式中的實(shí)力構(gòu)造器可以設(shè)置為protected辞友,以允許子類派生
- Singleton模式一般不要支持拷貝構(gòu)造函數(shù)和Clone接口栅哀,因?yàn)橛锌赡軙?dǎo)致多個對象實(shí)例,與Singleton模式的初衷相違背称龙。
- 如何實(shí)現(xiàn)多線程環(huán)境下安全的Singleton昌屉?注意對雙檢查鎖的正確實(shí)現(xiàn)。
享元模式FlyWeight
運(yùn)用共享技術(shù)有效地支持大量的細(xì)粒度對象
——《設(shè)計模式》GoF
- 動機(jī)
在軟件系統(tǒng)采用純粹對象方案的問題在于大量細(xì)粒度的對象會很快充斥在系統(tǒng)中茵瀑,從而帶來很高的運(yùn)行是代價——主要指內(nèi)存需求方面的代價间驮。
以下是一個示意性的偽碼,具體FlyWeight的實(shí)現(xiàn)可能千差萬別
而他的主要思想其實(shí)就是設(shè)置好一個對象池马昨,如果對象的銷毀則返回到池中竞帽,需要使用對象則可以從池中獲取所需要的對象,進(jìn)而把創(chuàng)建對象變?yōu)橐环N取用的模式鸿捧。而避免在在每次使用該對象的時候都重新創(chuàng)建屹篓。如此的方案,第一可以解決某個對象數(shù)量不可控的問題匙奴,第二也可以解決對于某些對象創(chuàng)建過程消耗很大的問題堆巧。
以下的代碼為一個字處理的系統(tǒng),把字體看做為一種對象泼菌。
嚴(yán)格意義上講谍肤,每個字符都對應(yīng)著他的字體。但實(shí)際在使用的過程中哗伯,一篇文章來說也就只有幾種字體對象而已荒揣,如果為每個對象都創(chuàng)建了一個字體對象,那么會造成字體對象的大量膨脹焊刹,并且這樣的膨脹也更是沒有意義的系任。
class Font {
private:
//unique object key
string key;
//object state
//....
public:
Font(const string& key){
//...
}
};
class FontFactory{
private:
//字體對象池
map<string,Font* > fontPool;
public:
Font* GetFont(const string& key){
//根據(jù)key來在池子中查找字體對象
map<string,Font*>::iterator item=fontPool.find(key);
if(item!=footPool.end()){
return fontPool[key]; //查找到的就返回這個字體對象
}
else{
//沒有被創(chuàng)建過的對象恳蹲,則新創(chuàng)建一個,并放入池中
Font* font = new Font(key);
fontPool[key]= font;
return font;
}
}
void clear(){
//...
}
};
通過以上的字體池的問題俩滥,可以避免一篇文章具有十萬個字符嘉蕾,用到了十萬個字體對象,而大量的字體對象都是重復(fù)的霜旧。FlyWeight共享的對象一旦創(chuàng)建則無法改變荆针,所以該對象應(yīng)該是只讀的。
要點(diǎn)總結(jié)
- 面向?qū)ο蠛芎玫慕鉀Q了抽相性的問題颁糟,但是作為一個運(yùn)行在機(jī)器中的程序?qū)嶓w航背,我們需要考慮對象的代價問題。Flyweight主要解決面向的代價問題棱貌,一般不觸及面向?qū)ο蟮某橄笮詥栴}玖媚。
- Flyweight采用對象共享的做法來降低系統(tǒng)中的對象的個數(shù),從而降低細(xì)粒度對象給系統(tǒng)帶來的內(nèi)存壓力婚脱。在具體實(shí)現(xiàn)方面今魔,要注意對像狀態(tài)的處理。
- 對象的數(shù)量太大障贸,從而導(dǎo)致對像內(nèi)存開銷加大——什么樣的數(shù)量才算大错森?這需要我們仔細(xì)根據(jù)具體應(yīng)用情況進(jìn)行評估,而不能憑空臆斷篮洁。