3.3 對享元對象的管理##
雖然享元模式對于共享的享元對象實例的管理要求,沒有實例池對實例管理的要求那么高,但是也還是有很多自身的特點功能,比如:引用計數、垃圾清除等。所謂垃圾,就是在緩存中存在,但是不再需要被使用的緩存中的對象。
所謂引用計數,就是享元工廠能夠記錄每個享元被使用的次數;而垃圾清除,則是大多數緩存管理都有的功能,緩存不能只往里面放數據,在不需要這些數據的時候,應該把這些數據從緩存中清除,釋放相應的內存空間,以節省資源。
在前面的示例中,共享的享元對象是很多人共享的,基本上可以一直存在于系統中,不用清除。但是垃圾清除是享元對象管理的一個很常見功能,還是通過示例給大家講一下,看看如何實現這些常見的功能。
- 實現引用計數的基本思路
要實現引用計數,就在享元工廠里面定義一個Map,它的key值跟緩存享元對象的key是一樣的,而value就是被引用的次數,這樣當外部每次獲取該享元的時候,就把對應的引用計數取出來加上1,然后再記錄回去。
- 實現垃圾回收的基本思路
要實現垃圾回收就比較麻煩點,首先要能確定哪些是垃圾?其次是何時回收?還有由誰來回收?如何回收?解決了這些問題,也就能實現垃圾回收了。
為了確定哪些是垃圾,一個簡單的方案是這樣的,定義一個緩存對象的配置對象,在這個對象中描述了緩存的開始時間和最長不被使用的時間,這個時候判斷是垃圾的計算公式如下:當前的時間 - 緩存的開始時間 >= 最長不被使用的時間。當然,每次這個對象被使用的時候,就把那個緩存開始的時間更新為使用時的當前時間,也就是說如果一直有人用的話,這個對象是不會被判斷為垃圾的。
何時回收的問題,當然是判斷出來是垃圾了就可以回收了。
關鍵是誰來判斷垃圾,還有誰來回收垃圾的問題。一個簡單的方案是定義一個內部的線程,這個線程在享元工廠被創建的時候就啟動運行。由這個線程每隔一定的時間來循環緩存中所有對象的緩存配置,看看是否是垃圾,如果是垃圾,那就可以啟動回收了。
怎么回收呢?這個比較簡單,就是直接從緩存的map對象中刪除掉相應的對象,讓這些對象沒有引用的地方,那么這些對象就可以等著被虛擬機的垃圾回收來回收掉了。
- 代碼示例
(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 享元模式的優缺點##
- 減少對象數量,節省內存空間
可能有的朋友認為共享對象會浪費空間,但是如果這些對象頻繁使用,那么其實是節省空間的。因為占用空間的大小等于每個對象實例占用的大小再乘以數量,對于享元對象來講,基本上就只有一個實例,大大減少了享元對象的數量,并節省不少的內存空間。
節省的空間取決于以下幾個因素:因為共享而減少的實例數目、每個實例本身所占用的空間。假如每個對象實例占用2個字節,如果不共享數量是100個,而共享過后就只有一個了,那么節省的空間約等于:(100-1) X 2 字節。
- 維護共享對象,需要額外開銷
如同前面演示的享元工廠,在維護共享對象的時候,如果功能復雜,會有很多額外的開銷,比如有一個線程來維護垃圾回收。
3.5 思考享元模式##
- 享元模式的本質
享元模式的本質:分離與共享。
分離的是對象狀態中變與不變的部分,共享的是對象中不變的部分。享元模式的關鍵之處就在于分離變與不變,把不變的部分作為享元對象的內部狀態,而變化部分就作為外部狀態,由外部來維護,這樣享元對象就能夠被共享,從而減少對象數量,并節省大量的內存空間。
理解了這個本質后,在使用享元模式的時候,就會去考慮,哪些狀態需要分離?如何分離?分離后如何處理?哪些需要共享?如何管理共享的對象?外部如何使用共享的享元對象?是否需要不共享的對象?等等問題。
把這些問題都思考清楚,找到相應的解決方法,那么享元模式也就應用起來了,可能是標準的應用,也可能是變形的應用,但萬變不離其宗。
- 何時選用享元模式
建議在如下情況中,選用享元模式:
如果一個應用程序使用了大量的細粒度對象,可以使用享元模式來減少對象數量;
如果由于使用大量的對象,造成很大的存儲開銷,可以使用享元模式來減少對象數量,并節約內存;
如果對象的大多數狀態都可以轉變為外部狀態,比如通過計算得到,或是從外部傳入等,可以使用享元模式來實現內部狀態和外部狀態的分離;
如果不考慮對象的外部狀態,可以用相對較少的共享對象取代很多組合對象,可以使用享元模式來共享對象,然后組合對象來使用這些共享對象;
3.6 相關模式##
- 享元模式與單例模式
這兩個模式可以組合使用。
通常情況下,享元模式中的享元工廠可以實現成為單例。另外,享元工廠里面緩存的享元對象,都是單實例的,可以看成是單例模式的一種變形控制,在享元工廠里面來單例享元對象。
- 享元模式與組合模式
這兩個模式可以組合使用。
在享元模式里面,存在不需要共享的享元實現,這些不需要共享的享元通常是對共享的享元對象的組合對象,也就是說,享元模式通常會和組合模式組合使用,來實現更復雜的對象層次結構。
- 享元模式與狀態模式
這兩個模式可以組合使用。
可以使用享元模式來共享狀態模式中的狀態對象,通常在狀態模式中,會存在數量很大的、細粒度的狀態對象,而且它們基本上都是可以重復使用的,都是用來處理某一個固定的狀態的,它們需要的數據通常都是由上下文傳入,也就是變化部分都分離出去了,所以可以用享元模式來實現這些狀態對象。
- 享元模式與策略模式
這兩個模式可以組合使用。
可以使用享元模式來實現策略模式中的策略對象,跟狀態模式一樣,在策略模式中也存在大量細粒度的策略對象,它們需要的數據同樣是從上下文傳入的,所以可以使用享元模式來實現這些策略對象。