【結構型模式十二】享元模式-2(Flyweight)

3.3 對享元對象的管理##

雖然享元模式對于共享的享元對象實例的管理要求,沒有實例池對實例管理的要求那么高,但是也還是有很多自身的特點功能,比如:引用計數、垃圾清除等。所謂垃圾,就是在緩存中存在,但是不再需要被使用的緩存中的對象

所謂引用計數,就是享元工廠能夠記錄每個享元被使用的次數;而垃圾清除,則是大多數緩存管理都有的功能,緩存不能只往里面放數據,在不需要這些數據的時候,應該把這些數據從緩存中清除,釋放相應的內存空間,以節省資源。

在前面的示例中,共享的享元對象是很多人共享的,基本上可以一直存在于系統中,不用清除。但是垃圾清除是享元對象管理的一個很常見功能,還是通過示例給大家講一下,看看如何實現這些常見的功能。

  1. 實現引用計數的基本思路

要實現引用計數,就在享元工廠里面定義一個Map,它的key值跟緩存享元對象的key是一樣的,而value就是被引用的次數,這樣當外部每次獲取該享元的時候,就把對應的引用計數取出來加上1,然后再記錄回去。

  1. 實現垃圾回收的基本思路

要實現垃圾回收就比較麻煩點,首先要能確定哪些是垃圾?其次是何時回收?還有由誰來回收?如何回收?解決了這些問題,也就能實現垃圾回收了。

為了確定哪些是垃圾,一個簡單的方案是這樣的,定義一個緩存對象的配置對象,在這個對象中描述了緩存的開始時間和最長不被使用的時間,這個時候判斷是垃圾的計算公式如下:當前的時間 - 緩存的開始時間 >= 最長不被使用的時間。當然,每次這個對象被使用的時候,就把那個緩存開始的時間更新為使用時的當前時間,也就是說如果一直有人用的話,這個對象是不會被判斷為垃圾的。

何時回收的問題,當然是判斷出來是垃圾了就可以回收了。

關鍵是誰來判斷垃圾,還有誰來回收垃圾的問題。一個簡單的方案是定義一個內部的線程,這個線程在享元工廠被創建的時候就啟動運行。由這個線程每隔一定的時間來循環緩存中所有對象的緩存配置,看看是否是垃圾,如果是垃圾,那就可以啟動回收了。

怎么回收呢?這個比較簡單,就是直接從緩存的map對象中刪除掉相應的對象,讓這些對象沒有引用的地方,那么這些對象就可以等著被虛擬機的垃圾回收來回收掉了

  1. 代碼示例

(1)分析了這么多,還是看代碼示例會比較清楚,先看緩存配置對象,示例代碼如下:

/**
   * 描述享元對象緩存的配置對象
   */
public class CacheConfModel{
      /**
       * 緩存開始計時的開始時間
       */
      private long beginTime;
      /**
       * 緩存對象存放的持續時間,其實是最長不被使用的時間
       */
      private double durableTime;
      /**
       * 緩存對象需要被永久存儲,也就是不需要從緩存中刪除
       */
      private boolean forever;
      public boolean isForever() {
         return forever;
      }
      public void setForever(boolean forever) {
         this.forever = forever;
      }
      public long getBeginTime() {
         return beginTime;
      }
      public void setBeginTime(long beginTime) {
         this.beginTime = beginTime;
      }
      public double getDurableTime() {
         return durableTime;
      }
      public void setDurableTime(double durableTime) {
         this.durableTime = durableTime;
      }
}

(2)對享元對象的管理的工作,是由享元工廠來完成的,因此上面的功能,也集中在享元工廠里面來實現,在上一個例子的基礎之上,來實現這些功能,改進后的享元工廠相對而言稍復雜一點,大致有如下改變:

添加一個Map,來緩存被共享對象的緩存配置的數據;

添加一個Map,來記錄緩存對象被引用的次數;

為了測試方便,定義了一個常量來描述緩存的持續時間;

提供獲取某個享元被使用的次數的方法;

在獲取享元的對象里面,就要設置相應的引用計數和緩存設置了,示例采用的是內部默認設置一個緩存設置,其實也可以改造一下獲取享元的方法,從外部傳入緩存設置的數據;

提供一個清除緩存的線程,實現判斷緩存數據是否已經是垃圾了,如果是,那就把它從緩存中清除掉;

基本上重新實現了享元工廠,示例代碼如下:

/**
   * 享元工廠,通常實現成為單例
   * 加入實現垃圾回收和引用計數的功能
   */
public class FlyweightFactory {
      private static FlyweightFactory factory = new FlyweightFactory();
      private FlyweightFactory(){
         //啟動清除緩存值的線程
         Thread t = new ClearCache();
         t.start();
      }
      public static FlyweightFactory getInstance(){
         return factory;
      }

      /**
       * 緩存多個flyweight對象
       */
      private Map<String,Flyweight> fsMap = new HashMap<String,Flyweight>();
      /**
       * 用來緩存被共享對象的緩存配置,key值和上面map的一樣
       */
      private Map<String,CacheConfModel> cacheConfMap = new HashMap<String,CacheConfModel>();
      /**
       * 用來記錄緩存對象被引用的次數,key值和上面map的一樣
       */
      private Map<String,Integer> countMap = new HashMap<String,Integer>();
      /**
       * 默認保存6秒鐘,主要為了測試方便,這個時間可以根據應用的要求設置
       */
      private final long DURABLE_TIME = 6*1000L;
  
      /**
       * 獲取某個享元被使用的次數
       * @param key 享元的key
       * @return 被使用的次數
       */
      public synchronized int getUseTimes(String key){
         Integer count = countMap.get(key);
         if(count==null){
             count = 0;
         }
         return count;
      }
      /**
       * 獲取key對應的享元對象
       * @param key 獲取享元對象的key
       * @return key對應的享元對象
       */
      public synchronized Flyweight getFlyweight(String key) {
         Flyweight f = fsMap.get(key);
         if(f==null){
             f = new AuthorizationFlyweight(key);
             fsMap.put(key,f);
             //同時設置引用計數
             countMap.put(key, 1);

             //同時設置緩存配置數據
             CacheConfModel cm = new CacheConfModel();
             cm.setBeginTime(System.currentTimeMillis());
             cm.setForever(false);
             cm.setDurableTime(DURABLE_TIME);
         
             cacheConfMap.put(key, cm);
         }else{
             //表示還在使用,那么應該重新設置緩存配置
             CacheConfModel cm = cacheConfMap.get(key);
             cm.setBeginTime(System.currentTimeMillis());
             //設置回去
             this.cacheConfMap.put(key, cm);
             //同時計數加1
             Integer count = countMap.get(key);
             count++;
             countMap.put(key, count);
         }
         return f;
      }
      /**
       * 刪除key對應的享元對象,連帶清除對應的緩存配置和引用次數的記錄,不對外
       * @param key 要刪除的享元對象的key
       */
      private synchronized void removeFlyweight(String key){
         this.fsMap.remove(key);
         this.cacheConfMap.remove(key);
         this.countMap.remove(key);
      }
      /**
       * 維護清除緩存的線程,內部使用
       */
      private  class ClearCache extends Thread{
         public void run(){
             while(true){
                Set<String> tempSet = new HashSet<String>();
                Set<String> set = cacheConfMap.keySet();
                for(String key : set){
                    CacheConfModel ccm = cacheConfMap.get(key);
                    //比較是否需要清除
                    if((System.currentTimeMillis() - ccm.getBeginTime()) >= ccm.getDurableTime()){
                       //可以清除,先記錄下來
                       tempSet.add(key);
                    }
                }
                //真正清除
                for(String key : tempSet){
                    FlyweightFactory.getInstance().removeFlyweight(key);
                }
                System.out.println("now thread="+fsMap.size() +",fsMap=="+fsMap.keySet());
                //休息1秒再重新判斷
                try {
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
             }
         }
      }
}

注意:getUseTimes、removeFlyweight和getFlyweight這幾個方法是加了同步的,原因是在多線程環境下使用它們,容易出現并發錯誤,比如一個線程在獲取享元對象,而另一個線程在刪除這個緩存對象。

(3)要想看出引用計數的效果來,SecurityMgr需要進行一點修改,至少不要再緩存數據了,要直接從享元工廠中獲取數據,否則就沒有辦法準確引用計數了,大致改變如下:

去掉了放置登錄人員對應權限數據的緩存;

不需要實現登錄功能,在這個示意程序里面,登錄方法已經不用實現任何功能,因此直接去掉;

原來通過map獲取值的地方,直接通過queryByUser獲取就好了;

示例代碼如下:

public class SecurityMgr {
      private static SecurityMgr securityMgr = new SecurityMgr();
      private SecurityMgr(){     
      }
      public static SecurityMgr getInstance(){
         return securityMgr;
      }
      /**
       * 判斷某用戶對某個安全實體是否擁有某權限
       * @param user 被檢測權限的用戶
       * @param securityEntity 安全實體
       * @param permit 權限
       * @return true表示擁有相應權限,false表示沒有相應權限
       */
      public boolean hasPermit(String user,String securityEntity,String permit){
         Collection<Flyweight> col = this.queryByUser(user);
         if(col==null || col.size()==0){
             System.out.println(user+"沒有登錄或是沒有被分配任何權限");
             return false;
         }
         for(Flyweight fm : col){
             if(fm.match(securityEntity, permit)){
                return true;
             }
         }
         return false;
      }
      /**
       * 從數據庫中獲取某人所擁有的權限
       * @param user 需要獲取所擁有的權限的人員
       * @return 某人所擁有的權限
       */
      private Collection<Flyweight> queryByUser(String user){
         Collection<Flyweight> col = new ArrayList<Flyweight>();
     
         for(String s : TestDB.colDB){
             String ss[] = s.split(",");
             if(ss[0].equals(user)){
                Flyweight fm = null;
                if(ss[3].equals("2")){
                    //表示是組合
                    fm = new UnsharedConcreteFlyweight();
                    //獲取需要組合的數據
                    String tempSs[] = TestDB.mapDB.get(ss[1]);
                    for(String tempS : tempSs){
                       Flyweight tempFm = FlyweightFactory.getInstance().getFlyweight(tempS);
                       //把這個對象加入到組合對象中
                       fm.add(tempFm);
                    }
                }else{
                    fm = FlyweightFactory.getInstance().getFlyweight(ss[1]+","+ss[2]);
                }            
                col.add(fm);
             }
         }
         return col;
      }  
}

(4)還是寫個客戶端來試試看,上面的享元工廠能否實現對享元對象的管理,尤其是對于垃圾回收和計數方面的功能,對于垃圾回收的功能不需要新加任何的測試代碼,而對于引用計數的功能,需要寫代碼來調用才能看到效果,示例代碼如下:

public class Client {
      public static void main(String[] args) throws Exception{
         SecurityMgr mgr = SecurityMgr.getInstance();
         boolean f1 = mgr.hasPermit("張三","薪資數據","查看");
         boolean f2 = mgr.hasPermit("李四","薪資數據","查看");
         boolean f3 = mgr.hasPermit("李四","薪資數據","修改");

         for(int i=0;i<3;i++){
             mgr.hasPermit("張三"+i,"薪資數據","查看");
         }  
     
         //特別提醒:這里查看的引用次數,不是指測試使用的次數,指的是
         //SecurityMgr的queryByUser方法通過享元工廠去獲取享元對象的次數
         System.out.println("薪資數據,查看 被引用了"+FlyweightFactory.getInstance().getUseTimes("薪資數據,查看")+"次");
         System.out.println("薪資數據,修改 被引用了"+FlyweightFactory.getInstance().getUseTimes("薪資數據,修改")+"次");
         System.out.println("人員列表,查看 被引用了"+FlyweightFactory.getInstance().getUseTimes("人員列表,查看")+"次");
      }
}

進行緩存的垃圾回收功能的是個線程在運行,所以你不終止該線程運行,程序會一直運行下去,運行部分結果如下:

薪資數據,查看 被引用了2次
薪資數據,修改 被引用了2次
人員列表,查看 被引用了6次
now thread=3,fsMap==[人員列表,查看, 薪資數據,查看, 薪資數據,修改]
now thread=3,fsMap==[人員列表,查看, 薪資數據,查看, 薪資數據,修改]
now thread=3,fsMap==[人員列表,查看, 薪資數據,查看, 薪資數據,修改]
now thread=3,fsMap==[人員列表,查看, 薪資數據,查看, 薪資數據,修改]
now thread=3,fsMap==[人員列表,查看, 薪資數據,查看, 薪資數據,修改]
now thread=3,fsMap==[人員列表,查看, 薪資數據,查看, 薪資數據,修改]
now thread=0,fsMap==[]
now thread=0,fsMap==[]

3.4 享元模式的優缺點##

  1. 減少對象數量,節省內存空間

可能有的朋友認為共享對象會浪費空間,但是如果這些對象頻繁使用,那么其實是節省空間的。因為占用空間的大小等于每個對象實例占用的大小再乘以數量,對于享元對象來講,基本上就只有一個實例,大大減少了享元對象的數量,并節省不少的內存空間

節省的空間取決于以下幾個因素:因為共享而減少的實例數目、每個實例本身所占用的空間。假如每個對象實例占用2個字節,如果不共享數量是100個,而共享過后就只有一個了,那么節省的空間約等于:(100-1) X 2 字節。

  1. 維護共享對象,需要額外開銷

如同前面演示的享元工廠,在維護共享對象的時候,如果功能復雜,會有很多額外的開銷,比如有一個線程來維護垃圾回收。

3.5 思考享元模式##

  1. 享元模式的本質

享元模式的本質:分離與共享。

分離的是對象狀態中變與不變的部分,共享的是對象中不變的部分。享元模式的關鍵之處就在于分離變與不變,把不變的部分作為享元對象的內部狀態,而變化部分就作為外部狀態,由外部來維護,這樣享元對象就能夠被共享,從而減少對象數量,并節省大量的內存空間。

理解了這個本質后,在使用享元模式的時候,就會去考慮,哪些狀態需要分離?如何分離?分離后如何處理?哪些需要共享?如何管理共享的對象?外部如何使用共享的享元對象?是否需要不共享的對象?等等問題。

把這些問題都思考清楚,找到相應的解決方法,那么享元模式也就應用起來了,可能是標準的應用,也可能是變形的應用,但萬變不離其宗。

  1. 何時選用享元模式

建議在如下情況中,選用享元模式:

如果一個應用程序使用了大量的細粒度對象,可以使用享元模式來減少對象數量;

如果由于使用大量的對象,造成很大的存儲開銷,可以使用享元模式來減少對象數量,并節約內存;

如果對象的大多數狀態都可以轉變為外部狀態,比如通過計算得到,或是從外部傳入等,可以使用享元模式來實現內部狀態和外部狀態的分離;

如果不考慮對象的外部狀態,可以用相對較少的共享對象取代很多組合對象,可以使用享元模式來共享對象,然后組合對象來使用這些共享對象;

3.6 相關模式##

  1. 享元模式與單例模式

這兩個模式可以組合使用。

通常情況下,享元模式中的享元工廠可以實現成為單例。另外,享元工廠里面緩存的享元對象,都是單實例的,可以看成是單例模式的一種變形控制,在享元工廠里面來單例享元對象。

  1. 享元模式與組合模式

這兩個模式可以組合使用。

在享元模式里面,存在不需要共享的享元實現,這些不需要共享的享元通常是對共享的享元對象的組合對象,也就是說,享元模式通常會和組合模式組合使用,來實現更復雜的對象層次結構。

  1. 享元模式與狀態模式

這兩個模式可以組合使用。

可以使用享元模式來共享狀態模式中的狀態對象,通常在狀態模式中,會存在數量很大的、細粒度的狀態對象,而且它們基本上都是可以重復使用的,都是用來處理某一個固定的狀態的,它們需要的數據通常都是由上下文傳入,也就是變化部分都分離出去了,所以可以用享元模式來實現這些狀態對象。

  1. 享元模式與策略模式

這兩個模式可以組合使用。

可以使用享元模式來實現策略模式中的策略對象,跟狀態模式一樣,在策略模式中也存在大量細粒度的策略對象,它們需要的數據同樣是從上下文傳入的,所以可以使用享元模式來實現這些策略對象。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,428評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,024評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,285評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,548評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,328評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,878評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,971評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,098評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,616評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,554評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,725評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,243評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,971評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,361評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,613評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,339評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,695評論 2 370

推薦閱讀更多精彩內容