* 24種設計模式——享元模式
核心:使用共享物件可有效地支援大量的細粒度的物件。運用共享技術,使得一些細粒度的物件可以共享。
一、報名系統crash多臺機器
報考系統crash,原因是使用了工廠模式來獲取物件,在大訪問量100萬時,就會有100萬個物件,因為JVM回收不及時,導致記憶體OutOfMemory,這裡我們可以把物件獲取換成一種"池"的形式
1.報考物件
2. 帶物件池的報考資訊public class SignInfo { //報名人員的ID private String id; //考試地點 private String location; //考試科目 private String subject; //郵寄地址 private String postAddress; get/set() }
考試科目和考試地點是有限的,我們可以把它做為key,在池中生成有限的物件。
3. 帶物件池的工廠類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; } }
4. 場景類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; } }
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----直接從池中取得
二、享元模式的定義
享元模式是池技術的重要實現方式。
定義:使用共享物件可有效地支援大量的細粒度的物件。
享元模式中,要求細粒度物件,那麼不可避免地使得物件數量多且性質相近,那我們就將這些物件的資訊分為兩個部分:內部狀態(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等,可以大幅提升效率。