* 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等,可以大幅提升效率。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值