Java设计模式之——享元模式

时间:2022-10-01 23:18:03

享元模式简单介绍

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

享元对象中的部分状态是可以共享,可以共享的状态称为内部状态,内部状态不会随着环境变化;不可共享的状态则称之为外部状态,它们会随着环境的改变而改变。在享元模式中会建立一个对象容器,在经典的享元模式中该容器为一个 Map,它的键是享元对象的内部状态,它的值就是享元对象本身。客户端程序通过这个内部状态从享元工厂中获取享元对象,如果有缓存则使用缓存对象,否则创建一个享元对象并且存入容器中,这样一来就避免了创建过多对象的问题。

享元模式定义

使用共享对象可以有效地支持大量的细粒度的对象。

享元模式的 UML 类图

Java设计模式之——享元模式

角色介绍 :

  • Flyweight:享元对象抽象基类或者接口。
  • ConcreteFlyweight:具体的享元对象。
  • FlyweightFactory:享元工厂,负责管理享元对象池和创建享元对象。

享元模式实战

过年回家买火车票是一件很困难的事,无数人用刷票的插件软件在向服务器发出请求,对于每一个请求服务器都必须做出应答。在用户设置好出发地和目的地之后,每次请求都返回一个查询的车票结果。为了便于理解,我们假设每次返回的只有一趟列车的车票。那么当数以万计的人不间断在请求数据时,如果每次都重新创建一个查询的车票结果,那么必然会造成大量重复对象的创建、销毁,使得 GC 任务繁重、内存占用率高居不下。而这类问题通过享元模式就能够得到很好地改善,从城市 A 到 城市B 的车辆是有限的,车上的铺位也就是硬卧、软卧、坐票 3 种。我们将这些可以公用的对象缓存起来,在用户查询是优先使用缓存,如果没有缓存则重新创建。这样就将成千上万的对象变成了可选择的有限数量。

首先我们创建一个 TIcker 接口,该接口定义展示车票信息的函数,具体代码如下所示:

public  interface Ticker {
void showTicketInfo(String bunk);
}

/**
* 具体的 Ticket 实现类 TrainTicket 火车票
*/

public class TrainTicket implements Ticker {
public String from; //始发地
public String to; //目的地
public String bunk; //铺位
public int price; //价位

TrainTicket(String from, String to) {
this.from = from;
this.to = to;
}

@Override
public void showTicketInfo(String bunk) {
price = new Random().nextInt(300);
Log.d("当前火车票信息", "购买从" + from + "到" + to + "的" + bunk + "火车票" + ",价格:" + price);
}
}

数据库中表示火车票的信息有出发地、目的地、铺位、价格等字段,在购票用户每次查询时如果没有用某种缓存模式,那么返回车票数据的接口实现如下:

public class TicketFactory {
public static Ticker getTicket(String from, String to) {
return new TrainTicket(from, to);
}
}

在 TicketFactory 的 getTicker 函数中每次会 new 一个 TrainTicker 对象,也就是说如果在短时间内有 10000用户来求购北京到青岛的车票,那么北京到青岛的车票对象就会被创建 10000 次,当数据返回之后这些对象变得无用了又会被虚拟机回收。此时就会造成大量的重复对象存在内存中,GC 对这些对象的回收也会非常消耗资源。如果用户的请求量很大可能导致系统变得极其缓慢,甚至可能导致 OOM。

正如上文所说,享元模式通过消息池的形式有效地减少了重复对象的存在。它通过内部状态标识某个种类的对象,外部程序根据这个不会变化的内部状态从消息池中取出对象。使得同一类对象可以被复用,避免大量重复对象。

使用享元模式很简单,只需要简单地改造一下 TIcketFactory,具体代码如下:

public class TicketFactory {
static Map<String, Ticker> sTicketMap = new ConcurrentHashMap<>();

public static Ticker getTicket(String from, String to) {
String key = from + "-" + to;
if (sTicketMap.containsKey(key)) {
return sTicketMap.get(key);
} else {
Ticker ticker = new TrainTicket(from,to);
sTicketMap.put(key,ticker);
return ticker;
}
}
}

我们在 TicketFactory 添加了一个 map 容器,并且以出发地 +“-” + 目的地为键、车票对象对值类存储车票对象。这个map 的键就是我们所说的内部状态,在这里就是出发地、横杠、目的地拼接起来的字符串,如果没有缓存则创建一个对象,并且将这个对象缓存到 map 中,下次再有这类请求时则直接从缓存中获取。这样即使有 10000 个请求从北京到青岛的车票信息,那么出发地是北京、目的地是青岛的对象只有一个。这样这个对象就从 10000 减到了 1 个,避免了大量的内存占用及频繁的 GC 操作。简单实现代码如下:

public class Test {
public static void main() {
Ticker ticker1 = TicketFactory.getTicket("北京", "青岛");
ticker1.showTicketInfo("软卧");

Ticker ticker2 = TicketFactory.getTicket("北京", "青岛");
ticker1.showTicketInfo("硬卧");

Ticker ticker3 = TicketFactory.getTicket("北京", "青岛");
ticker1.showTicketInfo("硬座");
}
}

总结

享元模式实现比较简单,但是它的作用在某些场景确实极其重要的。它可以大大减少应用程序创建的对象,降低程序内存的占用,增强程序的性能,但它同时也提高了系统的复杂性,需要分离出外部状态和内部状态,而且外部状态具有固化特性,不应该随着内部状态改变而改变,否则将导致系统的逻辑混乱。

享元模式的优点在于它大幅度地降低内存中对象的数量,但是,它做到这一点所付出的代价也是很高的。

  • 享元模式使得系统更加复杂,为了是对象可以共享,需要将一些状态外部化,这使得程序的逻辑复杂化。
  • 享元模式将享元对象的状态外部化,而读取外部状态使得运行事件稍微变长。