1.享元模式Flyweight
享元模式是一种结构型设计模式。多用于存在大量重复对象的场景或需要缓冲池的时候。常用于系统底层开发,解决系统的性能问题。
享元模式是对象池技术的重要实现方式,池里都是创建好的对象,无需再创建可直接拿来使用,这样减少了重复对象的创建,从而降低内存、提升性能。
Flyweight:抽象的享元角色(一般是接口或抽象类),是产品的抽象类,同时定义出对象的外部状态和内部状态的接口或实现。
ConcreteFlyweight:具体的享元角色,是具体的产品类,实现抽象角色,定义相关业务。
UnsharedConcreteFlyweight:不可共享的角色,一般不会出现在享元工厂。
FlyweightFactory:享元工厂类,用于构建一个池容器(集合),同时提供从池中获取对象的方法。
内部状态:指对象共享出来的信息,存储在享元对象内部且不会随环境的改变而改变。
外部状态:指对象得以依赖的一个标记,是随环境改变而改变的、不可共享的状态。
2.使用举例
比如人们浏览网站时,由于有各种各样的网站,大量的人使用同一个网站时,如果每次都创建新的网站对象,必然会造成大量重复对象的创建与销毁,此时可将这些可以共用的对象缓存起来,在用户查询时优先使用缓存,如果没有缓存则重新创建。对于某个网站来说,网站的类型type就是内部状态,而众多的使用者User就是外部状态。
抽象的享元角色接口(网站):
public abstract class Website {
public abstract void use(User user);
}
user作为外部状态,在构造方法中赋值。
具体的享元角色(具体的网站):
public class ConcreteWebsite extends Website {
private String type;
public ConcreteWebsite(String type) {
this.type = type;
}
@Override
public void use(User user) {
System.out.println(user.getName() + "正在使用" + type + "网站");
}
}
不可共享的角色(每个用户):
public class User {
private String name;
public User(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
享元工厂类:
public class WebsiteFactory {
private Map cache = new ConcurrentHashMap<>();
public Website getWebsite(String type) {
if (!cache.containsKey(type)) {
cache.put(type, new ConcreteWebsite( type));
}
return cache.get(type);
}
public int getCacheSize() {
return cache.size();
}
}
调用:
public class Client {
public static void main(String[] args) {
WebsiteFactory factory = new WebsiteFactory();
Website news = factory.getWebsite("新闻");
news.use(new User("Tom"));
Website blog = factory.getWebsite("博客");
blog.use(new User("Jack"));
Website blog2 = factory.getWebsite("博客");
blog2.use(new User("Smith"));
Website blog3 = factory.getWebsite("博客");
blog3.use(new User("Alice"));
System.out.println(factory.getCacheSize());
}
打印结果如下:
Tom正在使用新闻网站
Jack正在使用博客网站
Smith正在使用博客网站
Alice正在使用博客网站
2
可见,4个人在使用网站,而实际只创建了2个网站对象。这是因为在享元工厂类里,用map来保存各种不同的网站,以key查询,有重复就复用,没有就直接创建,避免了重复对象的大量创建。如果不使用享元模式,每个用户浏览一个网站都要创建一个网站的对象,当用户数据很大时势必会产生大量的内容重复的对象,当这些对象无用后GC回收将会非常耗费资源。
3.源码中的使用
Android中的Message、String、Parcel和TypedArray都利用了享元模式。
以Message为例,handler发送message如下:
public final boolean sendEmptyMessageAtTime( int what, long uptimeMillis) {
Message msg = Message.obtain();
msg.what = what;
return sendMessageAtTime(msg, uptimeMillis);
}
发送消息的时候最终调用sendEmptyMessageAtTime,在该方法里通过Message.obtain();创建message并发送。享元模式就是从obtain这里切入。
public final class Message implements Parcelable {
private static int sPoolSize = 0;
Message next;
private static final Object sPoolSync = new Object();
private static Message sPool;
private static final int MAX_POOL_SIZE = 10;
public static Message obtain() {
synchronized (sPoolSync) {
if (sPool != null) {
Message m = sPool; //m等于单链表
sPool = m.next; //sPool单链表舍弃表头元素
m.next = null; //m舍弃除表头之外的所有元素
m.flags = 0; //flag置0标记
sPoolSize--; //单链表大小减1
return m;
}
}
return new Message();
}
}
Message是一个单链表对象。Message中包含一个next的Message对象。sPoolSize表示个数。其中Message通过next成员变量持有对下一个Message的引用,从而构成了一个Message链表。Message Pool就通过该链表的表头管理着所有闲置的Message。
可以看到,当sPool为空时,就new Message返回一个新创建的对象;当sPool不为空时,就取出了单链表的头元素返回,同时单链表sPool舍弃表头,这样就返回了已创建的重复对象,完成了元素的复用。
一个Message在使用完后可以通过recycle()方法进入Message Pool,并在需要时通过obtain静态方法从Message Pool获取。
recycle实现代码如下:
public void recycle() {
if (isInUse()) {
if (gCheckRecycle) {
throw new IllegalStateException("This message cannot be recycled because it is still in use.");
}
return;
}
recycleUnchecked();
}
void recycleUnchecked() {
flags = FLAG_IN_USE;
what = 0;
arg1 = 0;
arg2 = 0;
obj = null;
replyTo = null;
sendingUid = -1;
when = 0;
target = null;
callback = null;
data = null;
synchronized (sPoolSync) {
if (sPoolSize < MAX_POOL_SIZE) { //单链表大小还没超过MAX_POOL_SIZE,则开始插入
next = sPool; //next直接等于自身
sPool = this; //sPool等于现在插入的元素
sPoolSize++; //大小+1
}
}
}
recycle()方法首先判断Message对象是否正在被使用,如果是则抛出异常,否则开始进行recycleUnchecked单链表插入操作。插入之前先清空了各个参数。
虽然Message并不是最标准的享元模式用法,但它通过单链表方式一样实现了对象池的思想。
另外,JDK中的String也是类似的消息池。一个String被定义过后就存在于常量池中。当其他地方使用相同的字符串时,实际使用的是缓存。
String str1 = new String("abc");
String str2 = new String("abc");
String str3 = "abc";
String str4 = "ab" + "c";
string一般有两个判断,一个是equals,这个是比较内容,以上四个都相等。另一个就是“==”判断,这个是比较引用是否相同。
Java中有个String池,这个String池就是利用享元模式,同样的字符串会从String池内获得。比如str3和str4就是从String池中取得的相同的对象。
str1不等于str2,不等于str3。但是,str3是等于str4。因为str1和str2是new的,str3是等号字面赋值,所以它们是不同的对象。而str4也是字面赋值,且str4使用了缓存在缓存池中的str3常量对象。所以用==判断的时候,他们是相同对象。
4.总结
享元模式摒弃了在每个对象中保存所有数据的方式,而是通过共享多个对象所共有的相同状态,从而在有限的内存容量中载入更多对象。
①享元与不可变性
由于享元对象可在不同的情景中使用,就必须确保其状态不能被修改。享元类的状态只能由构造函数的参数进行一次性初始化,它不能对其他对象公开其设置器或公有成员变量。
②享元工厂
为了能更方便地访问各种享元,可以创建一个工厂方法来管理已有享元对象的缓存池。工厂方法从客户端处接收目标享元对象的内在状态作为参数,如果它能在缓存池中找到所需享元, 则将其返回给客户端;如果没有找到,它就会新建一个享元,并将其添加到缓存池中。
注意:享元模式只是一种优化。在应用该模式之前要确定程序中存在有大量类似对象同时占用内存相关的内存消耗问题,并且确保该问题无法使用其他更好的方式来解决。
享元Flyweight类包含原始对象中部分能在多个对象中共享的状态。同一享元对象可在许多不同情景中使用。享元中存储的状态被称为“内在状态”。传递给享元方法的状态被称为“外在状态”。
情景Context类包含原始对象中各不相同的外在状态。情景与享元对象组合在一起就能表示原始对象的全部状态。
通常情况下,原始对象的行为会保留在享元类中。因此调用享元方法必须提供部分外在状态作为参数。但也可将行为移动到情景类中,然后将连入的享元作为单纯的数据对象。
客户端Client负责计算或存储享元的外在状态。在客户端看来,享元是一种可在运行时进行配置的模板对象,具体的配置方式为向其方法中传入一些情景数据参数。
享元工厂Flyweight Factory会对已有享元的缓存池进行管理。有了工厂后,客户端就无需直接创建享元,它们只需调用工厂并向其传递目标享元的一些内在状态即可。工厂会根据参数在之前已创建的享元中进行查找,如果找到满足条件的享元就将其返回;如果没有找到就根据参数新建享元。
享元模式优点:如果程序中有很多相似对象,那么将可以节省大量内存。
缺点:①可能需要牺牲执行速度来换取内存,因为他人每次调用享元方法时都需要重新计算部分情景数据。②代码会变得更加复杂。团队中的新成员总是会问:为什么要像这样拆分一个实体的状态?