設(shè)計(jì)模式-享元模式
定義
面向?qū)ο蠹夹g(shù)可以很好地解決一些靈活性或可擴(kuò)展性問題,但是很多情況下需要在系統(tǒng)中增加類和對(duì)象的個(gè)數(shù).當(dāng)對(duì)象數(shù)量太多時(shí),將導(dǎo)致運(yùn)行代價(jià)過高,帶來性能下降等問題.享元模式正是為解決這一類問題而誕生的.
享元模式(Flyweight Pattern)又稱為輕量級(jí)模式,是對(duì)象池的一種實(shí)現(xiàn).類似于線程池,線程池可以避免不停的創(chuàng)建和銷毀多個(gè)對(duì)象,消耗性能.提供了減少對(duì)象數(shù)量從而改善應(yīng)用所需的對(duì)象結(jié)構(gòu)的方式.其宗旨是共享細(xì)粒度對(duì)象,將多個(gè)對(duì)同一對(duì)象的訪問集中起來,不必為每個(gè)訪問者創(chuàng)建一個(gè)單獨(dú)的對(duì)象,從此來降低內(nèi)存的消耗,屬于結(jié)構(gòu)型模式.
享元模式把一個(gè)對(duì)象的狀態(tài)分為內(nèi)部狀態(tài)和外部狀態(tài),內(nèi)部狀態(tài)即是不變的,外部狀態(tài)是變化的,然后通過共享不變的部分,達(dá)到減少對(duì)象數(shù)量并節(jié)約內(nèi)存的目的.
享元模式的本質(zhì)是緩存共享對(duì)象,降低內(nèi)存消耗.
首先,我們來看向遠(yuǎn)模式的通用UML類圖:
抽象享元角色(FlyWeight):享元對(duì)象抽象基類或者接口,同時(shí)定義出對(duì)象的外部狀態(tài)和內(nèi)部狀態(tài)的接口或?qū)崿F(xiàn);
具體享元角色(ConcreteFlyWeight):實(shí)現(xiàn)抽象角色定義的業(yè)務(wù).該角色的內(nèi)部狀態(tài)處理應(yīng)該與環(huán)境無關(guān),不能出現(xiàn)同時(shí)改變內(nèi)部狀態(tài)和外部狀態(tài)的操作;
享元工廠(FlyWeightFactory):負(fù)責(zé)管理享元對(duì)象池和創(chuàng)建享元對(duì)象.
適用場(chǎng)景
當(dāng)系統(tǒng)中多處需要同一組信息時(shí),可以把這些信息封裝到一個(gè)對(duì)象中,然后對(duì)該對(duì)象進(jìn)行緩存,這樣,一個(gè)對(duì)象就可以提供給多處需要使用的地方,避免大量同一對(duì)象的多次創(chuàng)建,消耗大量內(nèi)存空間.
享元模式其實(shí)就是工廠模式的一個(gè)改進(jìn)機(jī)制,享元模式 同樣要求創(chuàng)建一個(gè)或一組對(duì)象,并且就是通過工廠方法生成對(duì)象的,只不過享元模式為工廠方法增加了緩存這一功能.
主要應(yīng)用場(chǎng)景:
1、常常應(yīng)用于系統(tǒng)底層的開發(fā),以便解決系統(tǒng)的性能問題.
2、系統(tǒng)有大量相似對(duì)象、需要緩沖池的場(chǎng)景.
在生活中的享元模式也很常見,比如各中介機(jī)構(gòu)的房源共享,再比如全國社保聯(lián)網(wǎng).
使用享元模式實(shí)現(xiàn)共享池業(yè)務(wù)
我們舉個(gè)例子,我們每年春節(jié)為了搶到一張回家的火車票都要大費(fèi)周折,進(jìn)而出現(xiàn)了很多刷票軟件,刷票軟件會(huì)將我們填寫的信息緩存起來,然后定時(shí)檢查余票信息.搶票的時(shí)候,我們肯定要查詢下有沒有我們需要的票,這里我們假設(shè)一張火車的信息包含:出發(fā)站蛤迎、目的站、價(jià)格谱煤、座位類別.現(xiàn)在要求編寫一個(gè)查詢火車票查詢偽代碼,可以通過出發(fā)站、目的站查到相關(guān)票.
比如:要求通過出發(fā)站、目的站查詢火車票,那么我們只需構(gòu)建出火車票類對(duì)象,然后提供一個(gè)查詢出發(fā)站庞钢、目的站的接口給到客戶進(jìn)行查詢即可,具體代碼如下:
分析上面的代碼,我們發(fā)現(xiàn)客戶端進(jìn)行查詢時(shí),系統(tǒng)通過TicketFactory直接創(chuàng)建一個(gè)火車票對(duì)象,但是這樣做的話,當(dāng)某個(gè)瞬間如果有大量的用戶請(qǐng)求同一張票的信息時(shí),系統(tǒng)就會(huì)創(chuàng)建出大量該火車票對(duì)象,系統(tǒng)內(nèi)存壓力驟增.而其實(shí)更好的做法應(yīng)該是緩存該票對(duì)象,然后復(fù)用提供給其他查詢請(qǐng)求,這樣一個(gè)對(duì)象就足以支撐數(shù)以千計(jì)的查詢請(qǐng)求,對(duì)內(nèi)存完全無壓力,使用享元模式可以很好地解決這個(gè)問題.
我們繼續(xù)優(yōu)化代碼,只需要在TicketFactory類中進(jìn)行更改,增加緩存機(jī)制:
可以看到,除了第一次查詢創(chuàng)建對(duì)象后,后續(xù)查詢相同車次票信息都是使用緩存對(duì)象,無需創(chuàng)建新對(duì)象了.來看一下類結(jié)構(gòu)圖:
其中ITicket就是抽象享元角色,TrainTicket就是具體享元角色,TicketFactory就是享元工廠.有些小伙伴一定會(huì)有疑惑啦,這不就是注冊(cè)式的單例模式嗎?對(duì),這就是注冊(cè)式單例模式.孫然,結(jié)構(gòu)上很像,但是享元模式的重點(diǎn)在結(jié)構(gòu)上,而不是在創(chuàng)建對(duì)象上.后面看看享元模式在JDK源嗎匯總的一個(gè)應(yīng)用,大家應(yīng)該就能徹底明白了.
再比如:我們經(jīng)常使用的數(shù)據(jù)庫連接池,因?yàn)槲覀兪褂肅onnection對(duì)象時(shí)主要性能消耗在建立連接和關(guān)閉連接的時(shí)候,為了提高Connection在調(diào)用時(shí)的性能,我們將Connection對(duì)象在調(diào)用前創(chuàng)建好緩存起來,用的時(shí)候從緩存中取值,用完再放回去,達(dá)到資源重復(fù)利用的目的.來看下面代碼:
這樣的連接池,普遍應(yīng)用于開源框架,有效提升底層的運(yùn)行性能.
享元模式在源碼中的應(yīng)用
1、String中的享元模式
Java中將String定義為final(不可改變的),JVM中字符串一般保存在字符串常量池中,java會(huì)確保一個(gè)字符串在常量池中只有一個(gè)拷貝,這個(gè)字符串常量池在JDK6.0以前是位于常量池中,位于永久代,而在JDK7.0中,JVM將其從永久代拿出來放置于堆上.當(dāng)創(chuàng)建一個(gè)字符串時(shí),如果字符串常量池中有該對(duì)象對(duì)應(yīng)的字面量,則返回該字面量在字符串常量池中的引用,否則,創(chuàng)建復(fù)制一份該字面量到字符串常量池并返回它的引用.
2因谎、Integer中的享元模式
在Integer源碼中的valueOf()方法做了一個(gè)條件判斷,如果目標(biāo)值在-128到127之間,則直接從緩存中取值,否則新建對(duì)象.那JDK為何要這樣做呢?因?yàn)樵?128到127之間的數(shù)據(jù)在int范圍內(nèi)是使用最頻繁的,為了節(jié)省頻繁創(chuàng)建對(duì)象帶來的內(nèi)存消耗,這里就用到了享元模式,來提高性能.
3基括、Long中的享元模式
同上.
4、Apache Commons Pool2中的享元模式
一種對(duì)象池實(shí)現(xiàn).
享元模式的內(nèi)部狀態(tài)和外部狀態(tài)
享元模式的定義為我們提出了兩個(gè)要求:細(xì)粒度和共享對(duì)象.因?yàn)橐蠹?xì)粒度對(duì)象,所以不可避免地會(huì)使對(duì)象數(shù)量多且性質(zhì)相近,此時(shí)我們就將這些對(duì)象的信息分為兩個(gè)部分:內(nèi)部狀態(tài)和外部狀態(tài).
內(nèi)部狀態(tài)指對(duì)象共享出來的信息,存儲(chǔ)在享元對(duì)象內(nèi)部且不會(huì)隨環(huán)境的變化而變化;外部狀態(tài)指對(duì)象得以依賴的一個(gè)標(biāo)記,是隨環(huán)境變化而變化的财岔、不可共享的狀態(tài).
優(yōu)點(diǎn)
1风皿、減少對(duì)象的創(chuàng)建,降低內(nèi)存中對(duì)象的數(shù)量,降低系統(tǒng)的內(nèi)存,提高效率;
2、減少內(nèi)存之外的其他資源占用.
缺點(diǎn)
1匠璧、關(guān)注內(nèi)桐款、外狀態(tài),關(guān)注線程安全問題;
2、使系統(tǒng)夷恍、程序的邏輯復(fù)雜化.