享元模式的优缺点是什么?
简化版
享元模式的优点是减少对象数量、降低内存占用、提升对象复用率;缺点是增加状态拆分和工厂管理复杂度,外部状态维护不当会导致代码难懂或数据错误。
详细版
享元模式适合对象数量巨大且重复状态明显的场景。它通过共享内部状态,让系统不用为每个场景创建完整对象。
优点:
- 降低内存占用。
- 减少重复对象创建。
- 统一管理共享对象。
- 对只读资源复用很友好。
缺点:
- 需要拆分内部状态和外部状态。
- 客户端要维护更多上下文。
- 享元工厂可能变复杂。
- 缓存池可能膨胀。
- 可变共享状态会带来线程安全问题。
完整版教学
一、优点:减少内存占用
享元模式最大的价值是内存优化。
如果 10 万个对象中有大量重复字段,把重复字段抽成共享对象,就可以明显减少内存占用。
例如游戏中很多树共享同一张贴图和同一种渲染配置,贴图不应该被每棵树复制一份。
二、优点:减少对象创建成本
享元工厂会复用已经存在的享元对象,避免重复创建。
对于创建成本高的对象,例如字体、图片元数据、规则模板,这种复用能提升性能。
不过要注意,享元模式主要目标是共享对象状态,减少创建成本只是常见副作用。
三、优点:统一管理共享资源
享元工厂集中管理共享对象,使系统更容易控制对象数量和生命周期。
比如所有字体样式都从 FontStyleFactory 获取,就可以统计样式种类、限制缓存大小、观察命中率。
如果对象创建散落在业务代码中,这些管理就很难做。
四、缺点:状态拆分增加理解成本
享元模式要求开发者区分内部状态和外部状态。拆分后,对象本身不再包含完整信息,调用时必须携带上下文。
这会让代码从“对象自己知道一切”变成“对象 + 外部状态一起完成行为”。如果命名和边界不清楚,新人读代码会比较吃力。
五、缺点:外部状态管理更复杂
外部状态通常由客户端或上下文对象维护。
如果外部状态传错,例如把坐标、用户 ID、请求上下文传错,享元对象本身无法判断业务是否正确。
因此享元模式要求调用方更谨慎,最好用清晰的值对象封装外部状态,而不是散落一堆参数。
六、缺点:缓存池可能失控
享元工厂通常维护缓存池。如果 key 的种类持续增长,缓存池就可能占用越来越多内存。
例如把任意用户输入都作为享元 key,就可能产生大量几乎不会复用的对象。
这类场景需要限制缓存范围,或者选择普通对象创建,不要强行享元化。
七、缺点:可变共享对象有风险
共享对象一旦可变,就可能出现数据串扰。
一个地方修改享元内部状态,所有引用它的地方都会受影响。多线程场景下,还可能出现并发安全问题。
享元对象通常应该设计为不可变,或者至少把可变状态严格放到外部。
八、常见误区与追问
享元的收益可以用数量模型估算。原有 N=100000 个对象,每个内部状态1 KB、外部状态16 B;若内部状态只有 K=10 种,共享后内部部分约从100 MB 降到10 KB,外部状态约1.6 MB。代价是每次操作都要查找享元并携带外部状态,代码与并发边界更复杂。
| 检查维度 | 判定依据 |
|---|---|
| 主要收益 | 大幅减少高重复内部状态的内存 |
| 主要代价 | 状态拆分、查找和参数传递复杂化 |
before=N×(I+E);after=K×I+N×E
记忆钩子:N 很大、K 很小、I 很重时,享元才最划算。
- 误区:享元一定提升运行速度。 它主要优化内存,哈希查找与外部状态组装可能增加 CPU。
- 追问:垃圾回收压力为何可能下降? 重复对象减少后,分配数量和存活对象规模都会降低。
- 误区:共享比例低也值得使用。 K 接近 N 时节省很小,工厂复杂度可能超过收益。
- 追问:如何验证实际收益? 用堆分析或 JMH 对比分配率、存活内存与操作耗时。
- 追问:最大的正确性风险是什么? 错误共享可变外部状态,导致不同使用者互相污染。
九、加强记忆
享元模式的优点是省对象、省内存、省重复创建;缺点是要拆状态、管缓存、防共享污染。只有重复对象足够多时,这些额外复杂度才值得。