(目录)
享元模式Flyweight:通过共享对象减少内存加载消耗
享元模式的用意
享元模式以共享的⽅法⾼效地⽀持⼤量的细粒度对象,享元对象能做到共享的关键是区分内蕴状态和外蕴状态
。
⼀个内蕴状态是存储在享元对象内部的,并且是不会随环境改变⽽有所不同的,因此⼀个享元可以具有内蕴状态并可以共享。⼀个外蕴状态是随环境的改变 ⽽改变的,不可以共享的状态,享元对象的外蕴状态必须由客户端保存,并在享元对象被创建之后,在需要使⽤的时候各⽅⾯传⼊到享元对象内部,外蕴状态不可以 影响享元对象的内蕴状态,换句话说,它们是相互独⽴的.
享元模式应当在什么情况下使用
● ⼀个系统有⼤量的对象
● 这些对象耗费⼤量的内存
● 这些对象可以按照内蕴状态分成很多的组,当把外蕴对象从对象中删除时,每⼀个组都可以仅⽤⼀个对象代替
● 使⽤享元模式需要维护⼀个记录了系统已有的所有享元的表,⽽这需要耗费资源,因此应当在有⾜够多的享元实现可供共享时才值的使⽤享元模式
原理
享元模式的原理和实现都很简单,但是应用场景却相对狭窄,它和缓存模式、池化模式有所联系,却又有不同。
原始定义是:
摒弃在每个对象中保存所有数据的方式,通过共享多个对象所共有的相同状态,从而让我们能在有限的内存容量中载入更多对象。
从定义中可以发现,享元模式要解决的核心问题就是
节约内存空间
,使用的办法是找出相似对象之间的共有特征,然后复用这些特征。
UML 图是如何表示享元模式的,如下图:
从这个 UML 图中,享元模式包含的关键角色有四个:
享元类(Flyweight)
:定义了享元对象需要实现的公共操作方法。在该方法中会使用一个状态作为输入参数,也叫外部状态,由客户端保存,在运行时改变;享元工厂类(Flyweight Factory)
:管理一个享元对象类的缓存池。它会存储享元对象之间需要传递的共有状态,比如,按照大写英文字母来作为状态标识,这种只在享元对象之间传递的方式就叫内部状态。同时,它还提供了一个通用方法 getFlyweight(),主要通过内部状态标识来获取享元对象;可共享的具体享元类(ConcreteFlyweight)
:能够复用享元工厂内部状态并实现享元类公共操作的具体实现类;非共享的具体享元类(UnsharedConcreteFlyweight)
:不复用享元工厂内部状态,但实现享元类的具体实现类。
备注:
●
内部状态
:不会随环境改变而改变的状态,俗称不可变对象。 比如,在 Java 中 Integer 对象初始化就是缓存 -127 到 128 的值,无论怎么使用 Integer,这些值都不会变化。
●
外部状态
:随环境改变而改变的状态。 通常是某个对象所独有的,不能被共享,因此,也只能由客户端保存。之所以需要外部状态就是因为客户端需要不同的定制化操作。
UML 对应的代码实现:
/**
* 享元类
*/
public interface FlyWeight {
void operation(int state);
}
/**
* 享元工厂类
*/
public class FlyWeightFactory {
// 定义一个池容器
public Map<String, FlyWeight> pool = new HashMap<>();
public FlyWeightFactory() {
// 将对应的内部状态添加进去
pool.put("A", new ConcreteFlyweight("A"));
pool.put("B", new ConcreteFlyweight("B"));
pool.put("C", new ConcreteFlyweight("C"));
}
// 根据内部状态来查找值
public FlyWeight getFlyWeight(String key) {
if (pool.containsKey(key)) {
System.out.println("==== 享元池中有,直接复用,key:" + key);
return pool.get(key);
} else {
System.out.println("==== 享元池中没有,重新创建并复用,key:" + key);
FlyWeight flyWeightNew = new ConcreteFlyweight(key);
pool.put(key, flyWeightNew);
return flyWeightNew ;
}
}
public FlyWeight getUnSharedFlyWeight(String key) {
FlyWeight flyWeight = new UnSharedConcreteFlyweight(key);
System.out.println("=== 创建不共享的对象,key: " + key);
return flyWeight;
}
}
public class UnSharedConcreteFlyweight implements FlyWeight{
private String uniqueKey;
public UnSharedConcreteFlyweight(String uniqueKey) {
this.uniqueKey = uniqueKey;
}
@Override
public void operation(int state) {
System.out.println("=== 使用不共享的对象,内部状态:"+uniqueKey+",外部状态:"+state);
}
}
这段代码实现非常简单,不过这里你可能会联想到缓存模式,比如,LRU 缓存模式。但这两者是完全不同的设计意图
它们的本质区别
在于:享元模式要解决的问题是节约内存的空间大小,而缓存模式本质上是为了节省时间
。
回到上面的代码分析中,能看出享元模式封装的变化有:
● 对象内部状态的定义规则,比如,是通过字母共享状态,还是通过固定的数字来共享状态; ● 具体享元对象所实现的公共操作的逻辑。
所以说,享元模式本质上是通过创建更多的可复用对象的共有特征来尽可能地减少创建重复对象的内存消耗
。
使用场景
一般来讲,享元模式常用的使用场景有以下几种:
- 系统中存在大量重复创建的对象。比如,同一个商品的展示图片、详情介绍、文字介绍等,当自营商家系统调用或第三方商家调用时,商品系统可以使用同一个对象来节省内存空间;
- 可以使用外部特定的状态来控制使用的对象。比如,使用常用的中文汉字作为读取的标识,读取享元池中共享的多个中文汉字对象;
- 相关性很高并且可以复用的对象。比如,公司的组织结构人员基本信息、网站的分类信息列表等。
Java 的 Integer 享元设计
在 Java 中,享元模式一个常用的场景就是,使用数据类的包装类对象的 valueOf() 方法。比如,使用 Integer.valueOf() 方法时,实际的代码实现中有一个叫 IntegerCache 的静态类,它就是一直缓存了 -127 到 128 范围内的数值,如下代码所示,可以在 Java JDK 中的 Integer 类的源码中找到这段代码。
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
享元模式本质上在使用时就是找到不可变的特征,并缓存起来,当类似对象使用时从缓存中读取,以达到节省内存空间的目的。比如,在需要承接大流量的系统中使用图片,都知道高清图片即便是压缩后占用的内存空间也很大,那么在使用图片时节省内存空间就是首要考虑的设计因素,而享元模式可以很好地帮助解决这类问题场景。
使用享元模式的原因
使用享元模式的原因,主要有以下两个:
减少内存消耗,节省服务器成本
。 比如,当大量商家的商品图片、固定文字(如商品介绍、商品属性)在不同的网页进行展示时,通常不需要重复创建对象,而是可以使用同一个对象,以避免重复存储而浪费内存空间。由于通过享元模式构建的对象是共享的,所以当程序在运行时不仅不用重复创建,还能减少程序与操作系统的 IO 交互次数,大大提升了读写性能;聚合同一类的不可变对象,提高对象复用性
。 比如,Java 中的 Number 对象族类(Integet、Long、Double 等)都是使用了享元模式例子,通过缓存不同范围数值来重复使用相同的数值。
优缺点
通过上面的分析,我们可以得出使用享元模式主要有以下优点
:
● 可以极大减少内存中对象的数量,使得相同对象或相似对象在内存中只保存一份。
● 通过封装内存特有的运行状态,达到共享对象之间高效复用的目的。
缺点:
●
以时间换空间,间接增加了系统的实现复杂度
。比如,需要分离出内部状态和外部状态,其中,外部状态这个定义比较模糊,也很难统一,而内部状态除了一些常用的常量容易被找到以外,那些更通用的组件对象在不同的业务系统中其实是不容易被找到的,因为不同的人对相似对象的理解并不一致,这就需要对程序员的代码设计实现思维有一定的要求。●
运行时间更长,对于一些需要快速响应的系统并不合适
。享元模式的目的是节省空间,而没有说需要提供更短的时间,这适合数据类项目的使用,而不适合一些有高并发要求的系统。
总结:
享元模式为共享对象定义了一个很好的结构范例。不过,用好享元模式的关键在于找到不可变对象,比如,常用数字、字符等。
之所以做对象共享而不是对象复制的一个很重要的原因,就在于节省对象占用的内存空间大小。
缓存模式
和享元模式
最大的区别
就是:
享元模式强调的是空间效率
,比如,一个很大的数据模型对象如何尽量少占用内存并提供可复用的能力;
缓存模式强调的是时间效率
,比如:缓存秒杀的活动数据和库存数据等,数据可能会占用很多内存和磁盘空间,但是得保证在大促活动开始时要能及时响应用户的购买需求。也就是说,
两者本质上解决的问题类型是不同的
。
虽然享元模式应用不如缓存模式多,但是对于超大型数据模式来说,它却是非常有效的优化方法之一。特别是对于现在越来越多的数据系统来说,共享变得更加重要,因为复制虽然时间效率更高,但是空间上可能完全不够。
标签:享元,状态,缓存,对象,模式,Flyweight,共享,设计模式 From: https://blog.51cto.com/panyujie/6952904