(Boolan)C++設(shè)計模式 <九> ——單例模式(Singleton)和享元模式(FlyWeight)

“對象性能”模式

面向?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;
}


Singleton的UML

要點(diǎn)總結(jié)

  1. 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的UML

以下是一個示意性的偽碼,具體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é)

  1. 面向?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)行評估,而不能憑空臆斷篮洁。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末涩维,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子袁波,更是在濱河造成了極大的恐慌瓦阐,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件篷牌,死亡現(xiàn)場離奇詭異睡蟋,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)枷颊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進(jìn)店門戳杀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人夭苗,你說我怎么就攤上這事信卡。” “怎么了听诸?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵坐求,是天一觀的道長蚕泽。 經(jīng)常有香客問我晌梨,道長桥嗤,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任仔蝌,我火速辦了婚禮泛领,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘敛惊。我一直安慰自己渊鞋,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布瞧挤。 她就那樣靜靜地躺著锡宋,像睡著了一般。 火紅的嫁衣襯著肌膚如雪特恬。 梳的紋絲不亂的頭發(fā)上执俩,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天,我揣著相機(jī)與錄音癌刽,去河邊找鬼役首。 笑死,一個胖子當(dāng)著我的面吹牛显拜,可吹牛的內(nèi)容都是我干的衡奥。 我是一名探鬼主播,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼远荠,長吁一口氣:“原來是場噩夢啊……” “哼矮固!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起譬淳,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤乏屯,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后瘦赫,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體辰晕,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年确虱,在試婚紗的時候發(fā)現(xiàn)自己被綠了含友。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡校辩,死狀恐怖窘问,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情宜咒,我是刑警寧澤惠赫,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站故黑,受9級特大地震影響儿咱,放射性物質(zhì)發(fā)生泄漏庭砍。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一混埠、第九天 我趴在偏房一處隱蔽的房頂上張望怠缸。 院中可真熱鬧,春花似錦钳宪、人聲如沸揭北。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽搔体。三九已至,卻和暖如春半醉,著一層夾襖步出監(jiān)牢的瞬間嫉柴,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工奉呛, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留计螺,地道東北人。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓瞧壮,卻偏偏與公主長得像登馒,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子咆槽,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評論 2 354

推薦閱讀更多精彩內(nèi)容