设计模式——享元模式

享元模式

享元模式(Flyweight Pattern):使用共享对象可有效地支持大量的细粒度的对象。

享元模式是池技术的重要实现方式。

要求细粒度对象,那么不可避免地使得对象数量多且性质相近,那我们就将这些对象的信息分为两个部分:内部状态(intrinsic)与外部状态(extrinsic)。

  • 内部状态:对象可共享出来的信息,存储在享元对象内部并且不会随环境改变而改变
  • 外部状态:对象得以依赖的一个标记,是随环境改变而改变的、不可以共享的状态

享元模式角色名称:

  • Flyweight抽象享元角色:简单地说就是一个产品的抽象类,同时定义出对象的外部状态和内部状态的接口或实现。
  • ConcreteFlyweight具体享元角色:具体的一个产品类,实现抽象角色定义的业务。该角色中需要注意的是内部状态处理应该与环境无关,不应该出现一个操作改变了内部状态,同时修改了外部状态,这是绝对不允许的。
  • unsharedConcreteFlyweight不可共享的享元角色:不存在外部状态或者安全要求(如线程安全)不能够使用共享技术的对象,该对象一般不会出现在享元工厂中。
  • FlyweightFactory享元工厂:职责非常简单,就是构造一个池容器,同时提供从池中获得对象的方法。

抽象享元角色Flyweight:

public abstract class Flyweight { 
    //内部状态 
    private String intrinsic; 
    //外部状态 
    protected final String Extrinsic; 
    //要求享元角色必须接受外部状态 
    public Flyweight(String _Extrinsic){ 
        this.Extrinsic = _Extrinsic; 
    }
    //定义业务操作 
    public abstract void operate(); 
    //内部状态的getter/setter 
    public String getIntrinsic() { 
        return intrinsic; 
    }
    public void setIntrinsic(String intrinsic) { 
        this.intrinsic = intrinsic; 
    } 
}

具体享元角色ConcreteFlyweight1~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(){ 
        //业务逻辑 
    } 
}

注意:在程序开发中,确认只需要一次赋值的属性则设置为final类型,避免无意修改导致逻辑混乱,特别是Session级的常量或变量

享元工厂FlyweightFactory:

public class FlyweightFactory { 
    //定义一个池容器 
    private static HashMap<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;} 
}

享元模式的优点:

大大减少应用程序创建的对象,降低程序内存的占用,增强程序的性能。

享元模式的缺点:

提高了系统复杂性,需要分离出外部状态和内部状态,而且外部状态具有固化特性,不应该随内部状态改变而改变,否则导致系统的逻辑混乱。

享元模式的使用场景:

  • 系统中存在大量的相似对象。
  • 细粒度的对象都具备较接近的外部状态,而且内部状态与环境无关,也就是说对象没有特定身份。
  • 需要缓冲池的场景

享元模式的扩展

  • 线程安全的问题
  • 性能平衡

享元模式的实例

由于已有的程序存在内存溢出,而内存溢出对Java应用来说实在是太平常了,有以下两种可能:

  • 内存泄漏:无意识的代码缺陷,导致内存泄漏,JVM不能获得连续的内存空间。
  • 对象太多:产生的对象太多,内存被耗尽。

这里我们使用对象池技术减少对象数量,对象池(Object Pool)的实现有很多开源工具,比如Apache的commons-pool 就是一个非常不错的池工具,我们暂时还用不到这种重量级的工具,我们自己来设计一个共享对象池,需要实现如下两个功能:

  • 容器定义:定义一个池容器,在这个容器中容纳哪些对象。
  • 提供客户端访问的接口:提供一个接口供客户端访问,池中有可用对象时,可以直接从池中获得,否则建立一个新的对象,并放置到池中。

原类图:

(1)新类图:

增加了一个子类,实现带缓冲池的对象建立,同时在工厂类上增加了一个容器对象HashMap,保存池中的所有对象。

注意:在对象池中,对象一旦产生,必然有一个唯一的、可访问的状态标志该对象,而且池中的对象声明周期是由池容器决定,而不是由使用者决定的。

public class SignInfo {
	//报考人员ID
	private String id;
	//考试地点
	private String location;
	//考试科目
	private String subject;
	//邮寄地址
	private String postAddress;
	public String getId() {
		return id;
	}
	public void setId(String id) {
		this.id = id;
	}
	public String getLocation() {
		return location;
	}
	public void setLocation(String location) {
		this.location = location;
	}
	public String getSubject() {
		return subject;
	}
	public void setSubject(String subject) {
		this.subject = subject;
	}
	public String getPostAddress() {
		return postAddress;
	}
	public void setPostAddress(String postAddress) {
		this.postAddress = postAddress;
	}
}
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;
	}	
}
import java.util.HashMap;
public class SignInfoFactory {
	//池容器
	private static HashMap<String, SignInfo> pool = new HashMap<String,SignInfo>();
	//报名信息的对象工厂
	@Deprecated
	public static SignInfo SignInfo() {
		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;
	}	
}

给不用的代码添加@Deprecated注解,不要有删除投产中代码的念头,如果方法或类确实不再使用了,增加该注解,表示该方法或类已经过时,尽量不要再使用了,我们应该保持历史原貌,同时也有助于版本向下兼容,特别是在产品级研发中。

import com.sfq.action.SignInfo;
import com.sfq.action.SignInfoFactory;
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);
			}
		}
		SignInfo signInfo = SignInfoFactory.getSignInfo("科目1考试地点1");
	}
}

结果
......
科目3考试地点28-----建立对象,放到池中
科目3考试地点29-----建立对象,放到池中
科目1考试地点1-----直接从池中取得

(2)线程安全问题

只要使用Java开发都会遇到线程安全问题,享元模式有太大的几率发生线程不安全。假设,我们这里只使用考试科目作为对象池中对象的唯一标识,则对象池最多存在4个对象。那么,此时我们启动N多个线程来模拟,就会出现LiSi的编号,ZhangSan的考试地址的情况。

public class SignInfoFactory { 
	//池容器 
	private static HashMap<String,SignInfo> pool = new HashMap<String,SignInfo>(); 
	//从池中获得对象 
	public static SignInfo getSignInfo(String key){ 
		//设置返回对象 
		SignInfo result = null; 
		//池中没有该对象,则建立,并放入池中 
		if(!pool.containsKey(key)){ 
			result = new SignInfo(); 
			pool.put(key, result); 
		}else{ 
			result = pool.get(key); 
		}
		return result; 
	} 
}
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("线程不安全了!"); 
        } 
    }
}
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"); 
        while(true){ 
            signInfo.setId("ZhangSan"); 
            signInfo.setLocation("ZhangSan"); 
            (new MultiThread(signInfo)).start(); 
            signInfo.setId("LiSi"); 
            signInfo.setLocation("LiSi");
            (new MultiThread(signInfo)).start();
        } 
    } 
}

我们在使用享元模式时,对象池中的享元对象尽量多,多到足够满足业务为止。

(3)性能平衡

尽量使用Java基本类型作为外部状态。如果不考虑系统的修改风险,完全可以重新建立一个类作为外部状态,因为这才完全符合面向对象编程的理念。

public class ExtrinsicState { 
    //考试科目 
    private String subject; 
    //考试地点 
    private String location; 
    public String getSubject() { 
        return subject; 
    }
    public void setSubject(String subject) {
        this.subject = subject; 
    }
    public String getLocation() { 
        return location; 
    }
    public void setLocation(String location) { 
        this.location = location; 
    }
    @Override 
    public boolean equals(Object obj){ 
        if(obj instanceof ExtrinsicState){ 
            ExtrinsicState state = (ExtrinsicState)obj; 
            return state.getLocation().equals(location) && state.getSubject().equals(subject); 
        }
        return false; 
    }
    @Override 
    public int hashCode(){ 
        return subject.hashCode() + location.hashCode(); 
    } 
} 

注意:一定要覆写equals和hashCode方法,否则它作为HashMap中的key值是根本没有意义的,只有hashCode值相等,并且equals返回结果为true,两个对象才相等,也只有在这种情况下才有可能从对象池中查找获得对象。

public class SignInfoFactory { 
    //池容器 
    private static HashMap<ExtrinsicState,SignInfo> pool = new HashMap <ExtrinsicState,SignInfo>(); 
    //从池中获得对象 
    public static SignInfo getSignInfo(ExtrinsicState key){ 
        //设置返回对象 
        SignInfo result = null; 
        //池中没有该对象,则建立,并放入池中
        if(!pool.containsKey(key)){ 
            result = new SignInfo(); 
            pool.put(key, result); 
        }else{ 
            result = pool.get(key); 
        }
        return result; 
    } 
} 
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) + " ms"); 
    } 
} 

执行时间:172 ms
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<10000000;i++){ 
            SignInfoFactory.getSignInfo(key2); 
        }
        long tailTime = System.currentTimeMillis(); 
        System.out.println("执行时间:"+(tailTime - currentTime) + " ms"); 
    } 
} 

执行时间:78 ms

外部状态最好以Java的基本类型作为标志,如String、int等,可以大幅地提升效率。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

肥羊汤

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值