
享元模式不是对象池,别混用 String 和 Integer 的缓存机制
Java 里最常被误当作“享元模式”的,其实是 String 常量池和 Integer 的 valueOf() 缓存。它们是 JVM 层的优化手段,不是你写的享元实现。真要落地享元,核心是:**区分内部状态(共享)和外部状态(不共享),把后者从对象里剥离出来传入方法**。
常见错误现象:
– 写了个 Flyweight 类,但所有字段都塞进构造器,每次 new 都不同实例
– 把用户 ID、时间戳这种明显变化的数据硬塞进享元对象里
– 用 ConcurrentHashMap 存享元,却没控制 key 的生成逻辑,导致缓存爆炸
- 外部状态必须由客户端持有,并在调用
operation()时传入(比如flyweight.operation(context)) - 内部状态必须是 final、不可变、线程安全的(如字体名、颜色值、图标路径)
- 享元工厂负责唯一性控制——用
Map<key flyweight></key>+ 双重检查锁 orcomputeIfAbsent()即可,别自己手写同步块
ObjectPool 来自 Apache Commons Pool,它和享元模式无关但常被一起用
享元解决的是“减少相同对象重复创建”,ObjectPool 解决的是“避免频繁 new/destruct 开销”,目标相似,机制不同。一个管“要不要复用”,一个管“能不能复用”。两者可以共存,但不能替代。
使用场景:
– 数据库连接、HTTP 客户端实例、大尺寸缓冲区(如 ByteBuffer)
– 对象构造成本高,且生命周期可控(能 clear/reset)
立即学习“Java免费学习笔记(深入)”;
- 别拿
ObjectPool去池化String或Integer—— 它们本身轻量,池化反而增加 GC 压力 - 务必实现
PooledObjectFactory的validateObject()和destroyObject(),否则脏对象会泄漏或复用失败 - 注意
GenericObjectPoolConfig的maxIdle和minIdle:设太高吃内存,太低起不到复用效果;生产环境建议minIdle=0,避免空转占资源
手动实现享元工厂时,key 设计决定内存是否失控
享元对象能不能被复用,全看工厂怎么判断“两个请求是否该返回同一个实例”。key 错了,要么重复创建(浪费),要么误共享(状态污染)。
参数差异:
– 用 new Key(font, size, bold) 比用 font + "|" + size + "|" + bold 更安全(避免特殊字符、null 导致 hash 不一致)
– 如果 key 含业务 ID(如 tenantId),确认该 ID 是否真的属于内部状态——多数情况它属于外部状态,不该进 key
- 优先用 record(Java 14+)或自定义
equals()/hashCode()的不可变类作 key,别依赖toString() - 如果 key 组合维度多(比如 5 个字段),考虑用
Objects.hash(a,b,c,d,e)而不是手拼字符串,减少内存分配 - 监控
pool.size()或 map 的 entry 数量,突然增长说明 key 设计有歧义(比如把时间戳当内部状态)
性能陷阱:享元 + 同步 + 外部状态 = 隐形锁竞争
表面上享元省了对象创建,但如果每个 operation() 都要加锁处理外部状态,或者共享对象内部维护了非线程安全的缓存(如 SimpleDateFormat),性能反而比直接 new 更差。
容易踩的坑:
– 在享元里缓存 LocalDateTime.now() 结果,以为省计算,结果时间不准还线程不安全
– 用 ThreadLocal 存外部状态,但没重置,导致内存泄漏
– 享元方法里调用了外部服务(如 DB 查询),把本该并行的请求串行化了
- 享元对象自身必须无状态或只含不可变状态;所有可变逻辑移到外部或方法参数中
- 如果 operation 需要临时缓存,用
ThreadLocal<builder></builder>之类一次性的,用完remove() - 压测时对比 “享元版” 和 “直接 new 版” 的吞吐与 GC pause,尤其关注
Old Gen占用——享元省的是 young gen 分配,但若 key 泄漏,old gen 会悄悄涨
真正难的不是写工厂或池,是准确识别哪些数据该“内化”,哪些必须“外提”。这个边界划错一次,后面全是并发 bug 和内存泄漏。









