享元模式

2019/10/14 posted in  算法

享元模式介绍

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

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

享元模式定义

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

享元模式的使用场景

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

享元模式的简单示例

过年回家买火车票是一件很困难的事,无数人用刷票插件软件在向服务端发出请求,对于每一个请求服务器都必须做出应答。在用户设置好出发地和目的地之后,每次请求都返回一个查询的车票结果。当数以万计的人不间断在请求数据时,如果每次都重新创建一个查询的车票结果,那么必然会造成大量重复对象的创建、销毁,使得GC任务繁重、内存占用率居高不下。但是通过享元模式,我们将这些可以公用的对象缓存起来,在用户查询时优先使用缓存,如果没有缓存则重新创建。

首先创建一个Ticket接口,该接口定义展示车票信息的函数

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

具体实现类TrainTicket

class TrainTicket implements Ticket {
    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);
        System.out.println("购买从" + from + "到" + to + "的" + bunk + "火车票,价格: " + price);
    }
}

返回车票数据的接口

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

当遇到大量的请求时,这样的代码会造成大量的重复对象存在内存中,GC对这些对象的回收也会非常消耗资源。如果用户的请求量很大可能导致系统变得极其缓慢,设置可能导致OOM。

使用享元模式进行优化

//车票工厂,以出发地和目的地为key缓存车票
public class TicketFactory {
    static Map<String, Ticket> sTicketMap = new ConcurrentHashMap<String, Ticket>();
    
    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;
        }
    }
}

这个map的键就是我们说的内部状态。如果没有缓存则创建一个对象,并且将这个对象缓存到map中,下次再有这类请求时直接从缓存中获取。

总结

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

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