21 设计模式——享元模式

返回设计模式博客目录

介绍


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

享元模式是对象池的一种实现,代表轻量级的意思。用来尽可能减少内存使用量,它适合用于大量重复对象的场景,来缓存可共享的对象,达到对象共享,避免创建过多对象的效果,这样一来就可以提升性能,避免内存移除等。

享元模式中存在以下两种状态:

  • 内部状态,即不会随着环境的改变而改变的可共享部分;
  • 外部状态,指随环境改变而改变的不可以共享的部分。

享元模式的实现要领就是区分应用中的这两种状态,并将外部状态外部化。

优点

相同对象只要保存一份,这降低了系统中对象的数量,从而降低了系统中细粒度对象给内存带来的压力。

缺点

  • 为了使对象可以共享,需要将一些不能共享的状态外部化,这将增加程序的复杂性。
  • 读取享元模式的外部状态会使得运行时间稍微变长。

使用场景

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

结构与实现


模式包含以下主要角色。

  • Flyweight(抽象享元角色):接口或抽象类,可以同时定义出对象的外部状态和内部状态的接口或实现。
  • ConcreteFlyweight(具体享元角色):实现抽象享元角色中定义的业务。
  • UnsharedConcreteFlyweight(不可共享的享元角色):并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。该对象一般不会出现在享元工厂中。
  • FlyweightFactory(享元工厂):管理对象池和创建享元对象。

其结构图如下图所示。

享元模式的实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 非享元角色
class UnsharedConcreteFlyweight {
private String info;
UnsharedConcreteFlyweight(String info) {
this.info = info;
}
public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}
}
// 抽象享元角色
interface Flyweight {
void operation(UnsharedConcreteFlyweight state);
}
// 具体享元角色
class ConcreteFlyweight implements Flyweight {
private String key;
ConcreteFlyweight(String key) {
this.key = key;
System.out.println("具体享元"+key+"被创建!");
}
public void operation(UnsharedConcreteFlyweight outState) {
System.out.print("具体享元"+key+"被调用,");
System.out.println("非享元信息是:"+outState.getInfo());
}
}
// 享元工厂角色
class FlyweightFactory {
private HashMap<String, Flyweight> flyweights = new HashMap<>();
public Flyweight getFlyweight(String key) {
Flyweight flyweight = (Flyweight)flyweights.get(key);
if(flyweight != null) {
System.out.println("具体享元"+key+"已经存在,被成功获取!");
} else {
flyweight = new ConcreteFlyweight(key);
flyweights.put(key, flyweight);
}
return flyweight;
}
}

测试代码:

1
2
3
4
5
6
7
8
9
10
11
FlyweightFactory factory = new FlyweightFactory();
Flyweight f01 = factory.getFlyweight("a");
Flyweight f02 = factory.getFlyweight("a");
Flyweight f03 = factory.getFlyweight("a");
Flyweight f11 = factory.getFlyweight("b");
Flyweight f12 = factory.getFlyweight("b");
f01.operation(new UnsharedConcreteFlyweight("第1次调用a。"));
f02.operation(new UnsharedConcreteFlyweight("第2次调用a。"));
f03.operation(new UnsharedConcreteFlyweight("第3次调用a。"));
f11.operation(new UnsharedConcreteFlyweight("第1次调用b。"));
f12.operation(new UnsharedConcreteFlyweight("第2次调用b。"));

程序运行结果如下:

1
2
3
4
5
6
7
8
9
10
具体享元a被创建!
具体享元a已经存在,被成功获取!
具体享元a已经存在,被成功获取!
具体享元b被创建!
具体享元b已经存在,被成功获取!
具体享元a被调用,非享元信息是:第1次调用a。
具体享元a被调用,非享元信息是:第2次调用a。
具体享元a被调用,非享元信息是:第3次调用a。
具体享元b被调用,非享元信息是:第1次调用b。
具体享元b被调用,非享元信息是:第2次调用b。

示例


这个是一个买车票的例子,客户端通过输入起始地和目的地到服务器,服务器返回车票价格信息,一次请求总会产生一个车票价格信息对象,如果是成千上万的用户不停的请求势必会使得服务器产生大量的重复对象,为了避免不必要的内存开销,可以使用享元模式来优化这种情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 定义车票接口和展示车票信息的函数
public interface Ticket {
void showTicketInfo(String bunk);
}
// 火车票实现类
public class TrainTicket implements Ticket {
public String from; // 始发地:内部状态
public String to; // 目的地:内部状态
public String bunk; // 铺位:外部状态
public int price; // 价格:外部状态
public TrainTicket(String from, String to) {
this.from = from;
this.to = to;
}
@Override
public void showTicketInfo(String bunk) {
price = new Random().nextInt(300);
System.out.println("购买 从 "+from+" 到 "+to+" 的 "+bunk+" 火车票"+",价格 : "+price);
}
}
//
public class TicketFactory {
static Map<String,Ticket> sTicketMap = new ConcurrentHashMap<>();
public static Ticket getTicket(String from, String to){
String key = from + "-" + to;
if (sTicketMap.containsKey(key)){
System.out.println("使用缓存 ==> "+key);
return sTicketMap.get(key);
} else {
System.out.println("创建对象 ==> "+key);
Ticket ticket = new TrainTicket(from,to);
sTicketMap.put(key,ticket);
return ticket;
}
}
}

这种方式把对象缓存到了sTicketMap, key为 “from + “-“ + to”,这样避免了重复的起始地和目的地产生重复对象的情况。

ANDROID 源码中的实现


Handler 消息机制中的 Message 消息池就是使用享元模式复用了 Message 对象。

使用 Message 时一般会用到 Message.obtain 来获取消息。如果使用 new Message() 会构造大量的 Message 对象。obtain 方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static Message obtain() {
synchronized (sPoolSync) {
if (sPool != null) {
Message m = sPool;
sPool = m.next;
m.next = null;
// 清空 in-use 标记
m.flags = 0; // clear in-use flag
sPoolSize--;
return m;
}
}
return new Message();
}

sPoolSync 和 sPool 定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
*
* Defines a message containing a description and arbitrary data object that can be
* sent to a {@link Handler}. This object contains two extra int fields and an
* extra object field that allow you to not do allocations in many cases.
*
* <p class="note">While the constructor of Message is public, the best way to get
* one of these is to call {@link #obtain Message.obtain()} or one of the
* {@link Handler#obtainMessage Handler.obtainMessage()} methods, which will pull
* them from a pool of recycled objects.</p>
*/
public final class Message implements Parcelable {
...
// sometimes we store linked lists of these things
/*package*/ Message next;
public static final Object sPoolSync = new Object();
private static Message sPool;
private static int sPoolSize = 0;
...
}

sPoolSync 是一个对象锁,用于在获取 Message 对象时进行同步锁。
sPool 是一个静态的 Message 对象。
next 是一个 Message 对象,指向下一个 Message。

可以看出,Message 消息池没有使用 map 这样的容器,而是使用的链表。

那么这些 Message 是什么时候放入链表中的呢?我们在 obtain 函数中只看到了从链表中获取,并且看到存储。如果消息池链表中没有可用对象的时候,obtain 中则是直接返回一个通过 new 创建的 Message 对象,而且并没有存储到链表中。

Message 类有一个 recycle 方法,它用来回收消息,并且把回收掉的消息添加到对象池链表中。recycle 方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
public void recycle() {
// 判断消息是否还在使用
if (isInUse()) {
if (gCheckRecycle) {
throw new IllegalStateException("This message cannot be recycled because it "
+ "is still in use.");
}
return;
}
// 清空状态,并且将消息添加到消息池中
recycleUnchecked();
}

recycleUnchecked 方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void recycleUnchecked() {
// Mark the message as in use while it remains in the recycled object pool.
// Clear out all other details.
// 清空消息状态,设置该消息 in-use flag
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) {
next = sPool;
sPool = this;
sPoolSize++;
}
}
}

recycle 会将一个 Message 回收到一个全局的池。如果消息在使用就抛出异常,否则调用 recycleUnchecked。

recycleUnchecked 先清空字段,然后回收消息,将 sPool 指向当前消息,同时 size 加一。

Message 通过在内部构建一个链表来维护一个被回收的 Message 对象的对象池,当用户调用 obtain 时会优先从池中取,如果池中没有可以复用的对象则创建这个新的 Message 对象。这些新创建的 Message 对象在被使用完之后会被回收到这个对象池中,当下次再调用 obtain 时,它们就会被复用。

因为 Android 应用是事件驱动的,因此,如果通过 new 创建 Message 会产生大量的重复的 Message 对象,导致内存占用率高、频繁 GC 等问题,通过享元模式创建一个大小为 50 的消息池,避免了上述问题。