享元模式原理与实现

所谓“享元”,顾名思义就是被共享的单元。享元模式(Flyweight Design Pattern)是运用共享技术有效地最大限度地复用细粒度对象的一种模式。享元模式的意图是复用对象,节省内存,前提是享元对象是不可变对象,因为享元对象会被多处代码共享使用,避免一处代码对享元进行了修改,影响到其他使用它的代码。

具体来讲,当一个系统中存在大量重复对象的时候,如果这些重复的对象是不可变对象,我们就可以利用享元模式将对象设计成享元,在内存中只保留一份实例,供多处代码引用。这样可以减少内存中对象的数量,起到节省内存的目的。实际上,不仅仅相同对象可以设计成享元,对于相似对象,我们也可以将这些对象中相同的部分(字段)提取出来,设计成享元,让这些大量相似对象引用这些享元。

享元模式的代码实现非常简单,主要是通过工厂模式,在工厂类中,通过一个 Map 来缓存已经创建过的享元对象,来达到复用的目的。如果对象已经存在于享元池中,则不会再创建该对象了,而是共用享元池中内部数据一致的对象。这样就减少了对象的创建,同时也节省了对象所占用的内存空间。代码示例如下:

  1. // 享元工厂类
  2. public class FlyweightFactory {
  3. // 享元池,用来存储享元对象
  4. private static final Map<String, Flyweight> FLYWEIGHT_MAP = new HashMap<>();
  5. public static Flyweight getFlyweight(String type) {
  6. // 如果在享元池中存在对象,则直接获取
  7. if (FLYWEIGHT_MAP.containsKey(type)) {
  8. return FLYWEIGHT_MAP.get(type);
  9. } else {
  10. // 在享元池不存在,则新创建对象,并放入到享元池
  11. Flyweight flyweight = new Flyweight(type);
  12. FLYWEIGHT_MAP.put(type, flyweight);
  13. return flyweight;
  14. }
  15. }
  16. }

享元模式 vs 单例、缓存、对象池

在上面的讲解中,我们多次提到“共享”“缓存”“复用”这些字眼,那它跟单例、缓存、对象池这些概念有什么区别呢?我们来简单对比一下。

享元模式跟单例的区别
在单例模式中,一个类只能创建一个对象,而在享元模式中,一个类可以创建多个对象,每个对象被多处代码引用共享。实际上,享元模式有点类似于单例的变体:多例。区别这两种设计模式,不能光看代码实现,而是要看设计意图,也就是要解决的问题。尽管从代码实现上来看,享元模式和多例有很多相似之处,但从设计意图上来看,它们是完全不同的。应用享元模式是为了对象复用,节省内存,而应用多例模式是为了限制对象的个数。

享元模式跟缓存的区别
在享元模式的实现中,我们通过工厂类来“缓存”已经创建好的对象。这里的“缓存”实际上是“存储”的意思,跟我们平时所说的“数据库缓存”“CPU 缓存”“MemCache 缓存”是两回事。我们平时所讲的缓存,主要是为了提高访问效率,而非复用。

享元模式跟对象池的区别
虽然对象池、连接池、线程池、享元模式都是为了复用,但对象池、连接池、线程池等池化技术中的“复用”和享元模式中的“复用”实际上是不同的概念。池化技术中的“复用”可以理解为“重复使用”,主要目的是节省时间(比如从数据库池中取一个连接,不需要重新创建)。在任意时刻,每一个对象、连接、线程,并不会被多处使用,而是被一个使用者独占,当使用完成之后,放回到池中,再由其他使用者重复利用。享元模式中的“复用”可以理解为“共享使用”,在整个生命周期中,都是被所有使用者共享的,主要目的是节省空间。

享元模式的使用场景

在 Java Integer 的实现中,-128 到 127 之间的整型对象会被事先创建好,缓存在 IntegerCache 类中。当我们使用自动装箱或者 valueOf() 来创建这个数值区间的整型对象时,会复用 IntegerCache 类事先创建好的对象。这里的 IntegerCache 类就是享元工厂类,事先创建好的整型对象就是享元对象。

在 Java String 类的实现中,JVM 开辟一块存储区专门存储字符串常量,这块存储区叫作字符串常量池,类似于 Integer 中的 IntegerCache。不过,跟 IntegerCache 不同的是,它并非事先创建好需要共享的对象,而是在程序的运行期间,根据需要来创建和缓存字符串常量。

实际上,享元模式对 JVM 的垃圾回收并不友好。因为享元工厂类一直保存了对享元对象的引用,这就导致享元对象在没有任何代码使用的情况下,也并不会被 JVM 垃圾回收机制自动回收掉。因此,在某些情况下,如果对象的生命周期很短,也不会被密集使用,利用享元模式反倒可能会浪费更多的内存。所以,除非经过线上验证,利用享元模式真的可以大大节省内存,否则,就不要过度使用这个模式,为了一点点内存的节省而引入一个复杂的设计模式,得不偿失。