← 返回题目列表

享元工厂为什么重要?对象池和缓存应该怎么设计?

高频 中等 第 7 / 25 题 更新于 2026/07/28
享元模式享元工厂对象池

简化版

享元工厂负责统一创建、缓存和复用享元对象,是享元模式能否生效的关键。它通常用 Map 根据内部状态 key 管理对象池,并需要考虑 key 设计、线程安全、缓存大小和生命周期。

详细版

如果客户端可以随意 new 享元对象,共享就无法保证。因此享元模式通常需要一个工厂:

  • 根据内部状态生成唯一 key。
  • 查询对象池是否已有实例。
  • 已有则返回。
  • 没有则创建、缓存并返回。

设计享元工厂时要注意:key 必须能准确代表内部状态;缓存不能无限膨胀;多线程场景要使用线程安全容器或同步控制;享元对象最好不可变。

完整版教学

一、为什么需要享元工厂

享元模式的核心是共享。如果对象创建入口散落在各处,系统很难保证同样内部状态只创建一份。

享元工厂把创建入口集中起来:

Flyweight fw = factory.get(key);

客户端不直接关心对象是否已经存在,只向工厂请求。工厂决定返回已有对象还是创建新对象。

二、对象池的基本结构

最常见的对象池是一个 Map:

class FlyweightFactory {
    private final Map<String, Flyweight> pool = new HashMap<>();

    public Flyweight get(String key) {
        return pool.computeIfAbsent(key, ConcreteFlyweight::new);
    }
}

key 代表内部状态。相同 key 返回同一个享元对象。

三、key 设计很关键

key 必须完整表达内部状态。

例如树类型由树种、颜色、纹理决定,key 就不能只用树种。如果只用树种,绿色松树和黄色松树可能被错误复用。

常见 key 设计方式:

  • 简单字符串拼接,适合字段少、格式稳定的场景。
  • 使用不可变值对象作为 key,适合字段多的场景。
  • 使用枚举,适合内部状态集合固定的场景。

值对象作为 key 时,要正确实现 equals()hashCode()

四、线程安全怎么处理

如果多个线程同时访问享元工厂,普通 HashMap 可能出现并发问题。

可以使用 ConcurrentHashMap

class FontFactory {
    private final ConcurrentHashMap<FontKey, FontStyle> pool = new ConcurrentHashMap<>();

    public FontStyle get(FontKey key) {
        return pool.computeIfAbsent(key, FontStyle::new);
    }
}

computeIfAbsent 能减少重复创建风险。需要注意的是,创建函数本身不应该有复杂副作用,否则并发下排查问题会变麻烦。

五、缓存大小和生命周期

享元池不是越大越好。如果 key 种类无限增长,享元池可能变成内存泄漏源。

比如以用户输入的任意字符串作为 key,如果字符串种类非常多,缓存会持续膨胀。

可以考虑:

  • 限制享元对象种类。
  • 使用 LRU 缓存。
  • 使用弱引用或软引用,但要理解回收时机不稳定。
  • 定期清理长时间不用的享元。
  • 只对高频重复对象使用享元。

六、享元对象是否允许修改

享元对象最好不可变。如果享元对象允许修改内部状态,所有共享它的客户端都会受到影响。

例如共享字体样式对象,如果某个地方修改了字号,其他所有使用该样式的字符都会被影响。这种问题非常隐蔽。

因此享元对象的字段通常用 final,并避免暴露可变集合。

七、工厂和业务逻辑的边界

享元工厂只负责对象复用,不应该承载复杂业务流程。

例如通知模板享元工厂可以根据模板 ID 返回模板对象,但不应该在工厂里判断用户权限、发送频率、业务状态。那些逻辑属于应用服务或业务对象。

工厂越纯粹,享元池越容易测试和维护。

八、常见误区与追问

享元工厂不仅保存对象,还必须保证同一键映射到同一语义实例。并发线程同时请求 key=A 时,如果用普通 getput,可能短暂创建2个对象;computeIfAbsent 可让映射创建原子化,但映射函数本身仍应轻量且无递归更新。键还必须包含全部内部状态维度。

检查维度判定依据
缓存命中返回已有不可变享元
缓存未命中按完整键创建并安全发布
key -> Factory Map -> shared Flyweight

易错点:唯一性是“按语义键唯一”,不是整个系统永远只有一个享元。

  • 误区:享元工厂必须永久保存所有实例。 键空间无界时需要容量限制、弱引用或淘汰策略。
  • 追问:为什么键不能只用显示名称? 若字体大小也属于内部状态,漏进键会返回语义错误的共享对象。
  • 误区:ConcurrentHashMap 能让享元内部线程安全。 它只保护映射访问,共享对象自身仍应不可变或同步。
  • 追问:computeIfAbsent 会执行几次创建函数? 并发实现以映射结果原子性为目标,函数不应依赖可观察副作用。
  • 追问:如何测试工厂? 相同键断言同一引用,不同内部状态键断言语义不同,并做并发请求测试。

九、加强记忆

享元工厂是“共享入口”:它用 key 找内部状态,用 Map 管对象池,用线程安全和生命周期策略保证共享可控。没有统一工厂,享元模式很容易退化成各处随意创建对象。