享元模式——物件共享,避免建立多物件
1. 享元模式介紹
享元模式是物件池的一種實現,它的英文名稱叫做Flyweight,代表輕量級的意思。享元模式用來儘可能減少記憶體使用量,他適合用於可能存在大量重複物件的場景,來快取可共享的物件,達到物件共享、避免建立過多物件的效果,這樣一來就可以提升效能、避免記憶體溢位等。
享元物件中的部分狀態是可以共享,可以共享的狀態成為內部狀態,內部狀態不會隨著環境變化;不可共享得狀態則稱之為外部狀態;他們會隨著環境的改變而改變。在享元模式中會建立一個物件容器,在經典的享元模式中該容器為一個Map,它的鍵是享元物件的內部狀態,它的值就是享元物件本身。客戶端程式通過這個內部狀態從享元工廠中獲取享元物件,如果有快取則使用快取的物件,否則建立一個享元物件並且存入容器中,這樣一來就避免了建立過多物件的問題。
2.享元模式定義
使用共享物件可有效地支援大量的細粒度的物件。
3.享元模式的使用場景
- 系統中存在大量的相似物件;
- 細粒度的物件都具備較接近的外部狀態,而且內部狀態與環境無關,也就是說物件沒有特定身份;
- 需要緩衝池的場景。
4.享元模式的UML類圖
角色介紹:
- Flyweight:享元物件抽象基類或介面;
- ConcreteFlyweight:具體的享元物件;
- FlyweightFactory:享元工廠,負責管理享元物件池和建立享元物件;
5.享元模式的簡單示例
過年回家買過車票是一件很困難的事,無數人用刷票外掛在想伺服器端發出請求,對於每一個請求伺服器都必須作出應答。在使用者設定好出發地和目的地之後,每次請求都返回一個查詢的車票結果。為了必然會造成大量重複物件的建立、銷燬,使得GC任務繁重、記憶體佔用高居不下。而這類問題通過享元模式就能夠得到很好的改善,從A城市到B城市是有限的,車上的鋪位也就是軟臥、硬臥、坐票3種。我們將這些可以公用的物件快取起來,在使用者查詢時優先使用快取,如果沒有快取則重新建立。這樣就將成千上萬的物件變為可選擇的有限數量。
首先我們建立一個Ticket介面,該介面定義展示車票資訊的函式,具體程式碼如下:
public interface Ticket {
public void showTicketInfo(String bunk);
}
它的具體的實現類是TrainTicket類,具體程式碼如下:
public class TrainTicket implements Ticket {
public String from;// 始發地
public String to;// 目的地
public String bunk;// 鋪位
public int price;
public TrainTicket(String from, String to) {
this.from = from;
this.to = to;
}
@Override
public void showTicketInfo(String bunk) {
price = new Random().nextInt(300);
System.out.println("購買從" + from + "到" + to + "的" + bunk + "火車票"
+ ",價格:" + price);
}
}
資料庫中表示火車票的資訊有出發地、目的地、鋪位、價格等欄位,在購票使用者每次查詢時如果沒有某種快取模式,那麼返回車票資料的介面實現如下:
public class TicketFactory{
public static Ticket getTicket(String from,String to){
return new TrainTicket(from,to);
}
}
在TicketFactory的getTicket函式中每次會new一個TrainTicket物件,也就是說如果在短時間內有10000使用者求購北京到青島的車票,那麼北京到青島的車票物件就會被建立1000次,當資料返回之後這些物件變得無用了又會被虛擬機器回收。此時就會造成大量的重複物件存在記憶體中,GC對這些物件的回收也會非常消耗資源。如果使用者的請求量很大可能導致系統變得極其緩慢,甚至可能導致OOM。
正如上文所說,享元模式通過訊息池的形式有效地減少重複物件的存在。它通過內部狀態標識某個種類的物件,外部程式根據這個不會變化的內部狀態從訊息池中取出物件。使得同一類物件可以被複用,避免大量重複物件。
使用享元模式簡單,只需要簡單地改造一下TicketFactory,具體程式碼如下:
public class TicketFactory {
static Map<String, Ticket> sTicketMap = new ConcurrentHashMap<String, Ticket>();
public static Ticket getTicket(String from, String to) {
String key = from + "-" + to;
if (sTicketMap.containsKey(key)) {
System.out.println("使用快取==>" + key);
return sTicketMap.get(key);
} else {
System.out.println("建立物件==>" + key);
Ticket ticket = new TrainTicket(from, to);
sTicketMap.put(key, ticket);
return ticket;
}
}
}
我們在TicketFactory新增一個map容器,並且以出發地+“-”+目的為鍵、以車票物件作為值儲存車票物件。這個map的鍵就是我們說的內部狀態,如果沒有快取則建立一個物件,並且將這個物件快取到map中,下次再有這類請求時則直接從快取中獲取。這樣即使有10000個請求從北京到青島的車票資訊,那麼出發地是北京、目的地是青島的車票物件只有一個,這樣就從這個物件從10000減到了1個,避免了大量的記憶體佔用及頻繁的GC操作。簡單實現程式碼如下:
public class Test {
public static void main(String[] args) {
Ticket ticket01 = TicketFactory.getTicket("北京", "青島");
ticket01.showTicketInfo("上鋪");
Ticket ticket02 = TicketFactory.getTicket("北京", "青島");
ticket01.showTicketInfo("下鋪");
Ticket ticket03 = TicketFactory.getTicket("北京", "青島");
ticket01.showTicketInfo("坐票");
}
}
執行結果:
建立物件==>北京-青島
購買從北京到青島的上鋪火車票,價格:270
使用快取==>北京-青島
購買從北京到青島的下鋪火車票,價格:249
使用快取==>北京-青島
購買從北京到青島的坐票火車票,價格:256
從輸出結果可以看到,只有第一次查詢車票時建立了一個物件,後續的查詢都使用的是訊息池中的物件。這其實就是相當於一個物件快取,避免了物件的重複建立與回收。在這個例子中,內部狀態就是出發地和目的地,內部狀態不會變化;外部狀態就是鋪位和價格,價格會隨著鋪位的變化而變化。
總結
享元模式實現比較簡單,但是它的作用在某些場景卻是極其重要。它可以大大減少應用程式建立的物件,降低程式記憶體的佔用,增強程式的效能,但它同時也提高了系統的複雜性,需要分離出外部狀態和內部狀態,而且外部狀態具有固化特性,不應該隨內部狀態改變而改變,否則導致系統的邏輯混亂。
享元模式的優點在於它大幅度地降低記憶體中物件的數量。但是,它做到這一點所付出的代價也是很高的。
- 享元模式使得系統更加複雜。為了是物件可以共享,需要將一些狀態外部化,這使得程式的邏輯複雜化。
- 享元模式將享元物件的狀態外部化,而讀取外部狀態使得執行時間稍微變長。