1. 程式人生 > >* 24種設計模式——享元模式

* 24種設計模式——享元模式

核心:使用共享物件可有效地支援大量的細粒度的物件。運用共享技術,使得一些細粒度的物件可以共享。

一、報名系統crash多臺機器

報考系統crash,原因是使用了工廠模式來獲取物件,在大訪問量100萬時,就會有100萬個物件,因為JVM回收不及時,導致記憶體OutOfMemory,這裡我們可以把物件獲取換成一種"池"的形式


1.報考物件

public class SignInfo {
	//報名人員的ID
	private String id;
	//考試地點
	private String location;
	//考試科目
	private String subject;
	//郵寄地址
	private String postAddress;
	get/set()
}
2. 帶物件池的報考資訊

考試科目和考試地點是有限的,我們可以把它做為key,在池中生成有限的物件。

public class SignInfo4Pool extends SignInfo{
	//定義一個物件池提取的KEY值
	private String key;
	//建構函式獲得相同標誌
	public SignInfo4Pool(String key) {
		this.key = key;
	}
	public String getKey() {
		return key;
	}
	public void setKey(String key) {
		this.key = key;
	}
}
3. 帶物件池的工廠類
public class SignInfoFactory {
	//池容器
	private static Map<String,SignInfo> pool = new HashMap<String,SignInfo>();
	//報名資訊的物件工廠
	@Deprecated
	public static SignInfo getSignInfo(){
		return new SignInfo();
	}
	//從池中獲得物件
	public static SignInfo getSignInfo(String key){
		//設定返回物件
		SignInfo result = null;
		//池中沒有該物件,則建立,並放入池中
		if(!pool.containsKey(key)){
			System.out.println(key+"----建立物件,並放置到池中");
			result = new SignInfo4Pool(key);
			pool.put(key, result);
		}else{
			result = pool.get(key);
			System.out.println(key+"----直接從池中取得");
		}
		return result;
	}
}
4. 場景類
public class Client {
	public static void main(String[] args) {
		//初始化物件池
		for(int i = 0;i < 4;i++){
			String subject = "科目"+i;
			//初始化地址
			for(int j = 0;j < 30;j++){
				String key = subject + "考試地點"+j;
				SignInfoFactory.getSignInfo(key);
			}
		}
		//System.out.println(SignInfoFactory.pool.size());//120
		SignInfo signInfo = SignInfoFactory.getSignInfo("科目1考試地點1");
	}
}

==>

科目3考試地點24----建立物件,並放置到池中
科目3考試地點25----建立物件,並放置到池中
科目3考試地點26----建立物件,並放置到池中
科目3考試地點27----建立物件,並放置到池中
科目3考試地點28----建立物件,並放置到池中
科目3考試地點29----建立物件,並放置到池中

............
科目1考試地點1----直接從池中取得

通過改造後,記憶體中就只有120個物件,相比之前100萬個SignInfo物件優化了非常多。

二、享元模式的定義


享元模式是池技術的重要實現方式。

定義:使用共享物件可有效地支援大量的細粒度的物件。
享元模式中,要求細粒度物件,那麼不可避免地使得物件數量多且性質相近,那我們就將這些物件的資訊分為兩個部分:內部狀態(intrinsic)與外部狀態(extrinsic)。

a. 內部狀態

內部狀態是物件可共享出來的資訊,儲存在享元物件內部並且不會隨環境改變而改變,如我們例子中的id/postAddress等,它們可以作為一個物件的動態附加資訊,不必直接儲存在具體某個物件中,屬於可以共享的部分。
b. 外部狀態

外部狀態是物件得以依賴的一個標記,是隨環境改變而改變的、不可以共享的狀態,如我們例子中的考試科目+考試地點複合字符串,它是一批物件的統一標識,是唯一的一個索引值。
1. 抽象享元角色

public abstract class Flyweight {
	//內部狀態
	private String intrinsic;
	//外部狀態
	protected final String Extrinsic;
	//要求享元角色必須接受外部狀態
	public Flyweight(String extrinsic) {
		Extrinsic = extrinsic;
	}
	//定義業務操作
	public abstract void operate();
	//內部狀態的getter/setter
	public String getIntrinsic() {
		return intrinsic;
	}
	public void setIntrinsic(String intrinsic) {
		this.intrinsic = intrinsic;
	}
}
2. 具體享元角色
public class ConcreteFlyweight1 extends Flyweight{
	//接受外部狀態
	public ConcreteFlyweight1(String extrinsic) {
		super(extrinsic);
	}
	//根據外部狀態進行邏輯處理
	public void operate() {
		//業務邏輯
	}
}
&
public class ConcreteFlyweight2 extends Flyweight{
	//接受外部狀態
	public ConcreteFlyweight2(String extrinsic) {
		super(extrinsic);
	}
	//根據外部狀態進行邏輯處理
	public void operate() {
		//業務邏輯
	}
}
3. 享元工廠
public class FlyweightFactory {
	//定義一個池容器
	private static Map<String,Flyweight> pool = new HashMap<String,Flyweight>();
	//享元工廠
	public static Flyweight getFlyweight(String extrinsic){
		//需要返回的物件
		Flyweight flyweight = null;
		//在池中沒有該物件
		if(pool.containsKey(extrinsic)){
			flyweight = pool.get(extrinsic);
		}else{
			//根據外部狀態建立享元物件
			flyweight = new ConcreteFlyweight1(extrinsic);
			//放置到池中
			pool.put(extrinsic, flyweight);
		}
		return flyweight;
	}
}

三、享元模式的應用

1. 優點和缺點

降低記憶體的佔用,但同時也提高了系統複雜性,需要分離出外部狀態和內部狀態,而且外部狀態具有固化特性,不應該隨內部狀態改變而改變,否則導致系統的邏輯混亂。

2. 使用場景 

1) 系統中存在大量的相似的物件

2) 細粒度的物件都具備較接近的外部狀態,而且內部狀態與環境無關,也就是說物件沒有特定身份。

3) 需要緩衝池的場景

四、享元模式擴充套件

1. 執行緒安全問題

如果不和例子中一樣使用“考試科目+考試地點”作為外部狀態,而只使用“考試科目”或者“考試地點”作為外部狀態呢,這樣池中的物件會更少,執行可以是可以,但是池中設定的享元物件數量太少,導致每個執行緒都到物件池中獲得物件,然後都去修改其屬性,於是就出現一些不和諧的資料,所以在使用享元模式時,物件池中的享元物件儘量多,多到足夠滿足業務為止。

1)報考資訊工廠

public class SignInfoFactory {
	//池容器
	private static Map<String,SignInfo> pool = new HashMap<String,SignInfo>();
	//報名資訊的物件工廠
	@Deprecated
	public static SignInfo getSignInfo(){
		return new SignInfo();
	}
	//從池中獲得物件
	public static SignInfo getSignInfo(String key){
		//設定返回物件
		SignInfo result = null;
		//池中沒有該物件,則建立,並放入池中
		if(!pool.containsKey(key)){
			System.out.println(key+"----建立物件,並放置到池中");
			result = new SignInfo();
//			result = new SignInfo4Pool(key);
			pool.put(key, result);
		}else{
			result = pool.get(key);
			System.out.println(key+"----直接從池中取得");
		}
		return result;
	}
}
2)多執行緒場景 
public class MultiThread extends Thread{
	private SignInfo signInfo;
	public MultiThread(SignInfo signInfo) {
		this.signInfo = signInfo;
	}
	public void run() {
		if(!signInfo.getId().equals(signInfo.getLocation())){
			System.out.println("編號:"+signInfo.getId());
			System.out.println("地址:"+signInfo.getLocation());
			System.out.println("執行緒不安全了");
		}
	}
}
3)場景類
public class Client {
	public static void main(String[] args) {
		//在物件池中初始化4個物件
		SignInfoFactory.getSignInfo("科目1");
		SignInfoFactory.getSignInfo("科目2");
		SignInfoFactory.getSignInfo("科目3");
		SignInfoFactory.getSignInfo("科目4");
		//取得物件
		SignInfo signInfo = SignInfoFactory.getSignInfo("科目2");
		signInfo.setId("123");
		SignInfo signInfo1 = SignInfoFactory.getSignInfo("科目2");
		System.out.println(signInfo1.getId());
	}
}
2. 效能平衡

儘量使用Java基本型別作為外部狀態(即HashMap中的Key),如果將例子中的外部狀態“科目”和“考點”用類封裝起來,這樣似乎更符合面向物件,但因為是外部狀態,是HashMap的key,所以要重寫類中的equals和hashCode,這樣才可以使用map的put或者get等,這樣執行效率就會大大下降。
1)外部狀態類

public class ExtrinsicState {
	//考試科目
	private String subject;
	//考試地點 
	private String location;
	get/set();
	public boolean equals(Object obj) {
		if(obj instanceof ExtrinsicState){
			ExtrinsicState state = (ExtrinsicState) obj;
			return state.getLocation().equals(location) && state.getSubject().equals(subject);
		}
		return false;
	}
	public int hashCode() {
		return subject.hashCode() + location.hashCode();
	}
}
2)享元工廠
public class SignInfoFactory {
	//池容器
	private static Map<ExtrinsicState,SignInfo> pool = new HashMap<ExtrinsicState,SignInfo>();
	//報名資訊的物件工廠
	@Deprecated
	public static SignInfo getSignInfo(){
		return new SignInfo();
	}
	//從池中獲得物件
	public static SignInfo getSignInfo(ExtrinsicState key){
		//設定返回物件
		SignInfo result = null;
		//池中沒有該物件,則建立,並放入池中
		if(!pool.containsKey(key)){
			System.out.println(key+"----建立物件,並放置到池中");
			result = new SignInfo();
			pool.put(key, result);
		}else{
			result = pool.get(key);
		}
		return result;
	}
}
3)場景類
public class Client {
	public static void main(String[] args) {
		//初始化物件池
		ExtrinsicState state1 = new ExtrinsicState();
		state1.setSubject("科目1");
		state1.setLocation("上海");
		SignInfoFactory.getSignInfo(state1);
		ExtrinsicState state2 = new ExtrinsicState();
		state2.setSubject("科目1");
		state2.setLocation("上海");
		//計算執行100萬次需要的時間
		long currentTime = System.currentTimeMillis();
		for(int i = 0;i < 1000000;i++){
			SignInfoFactory.getSignInfo(state2);
		}
		long tailTime = System.currentTimeMillis();
		System.out.println(tailTime-currentTime);
	}
}
==》103ms

不使用外部狀態類,用String型別代替

public class Client {
	public static void main(String[] args) {
		String key1 = "科目1上海";
		String key2 = "科目1上海";
		//初始化物件池
		SignInfoFactory.getSignInfo(key1);
		//計算執行10萬次需要的時間
		long currentTime = System.currentTimeMillis();
		for(int i = 0;i < 1000000;i++){
			SignInfoFactory.getSignInfo(key2);
		}
		long tailTime = System.currentTimeMillis();
		System.out.println(tailTime-currentTime);
	}
}
==》50ms

各位想想,使用自己編寫的類作為外部狀態,必須重寫equals方法和hashCode方法,而且執行效率還比較低,這種吃力不討好的事情最好別做,外部狀態最好以Java的基本型別作為標誌,如String、int等,可以大幅提升效率。