1092 字
3 分钟
享元(Flyweight)
一、为什么需要享元
一个文本编辑器打开了一份 50 万字的文档。每个字符都需要字体、大小、颜色等格式信息。如果每个字符都携带完整的格式对象,10 种字体 x 5 种大小 x 8 种颜色 = 400 种组合,但 50 万个字符可能创建了 50 万个格式对象,其中绝大多数是完全相同的。
游戏引擎里的 1000 棵树,每棵树有自己的位置和健康值(不同),但共享同一个网格模型和纹理(相同)。如果每棵树都复制一份模型数据,内存会被重复的网格和纹理撑爆,而 GPU 也无法批量渲染。
享元模式的核心洞察:将对象的状态分为内在的(可共享、不可变)和外在的(每个实例独有),只让每个唯一值存在一份。
二、现实类比
剧团的戏服衣橱。只有一套海盗服、一件王袍、一身农夫装。每个演这个角色的演员穿同一套共享戏服,而不是每人做一套私人定制的。演员的不同体现在各自的表演上,而不是戏服上。
三、核心思想
当成千上万对象有相同的不可变部分时,逐个分配浪费内存。享元维护一个规范实例池,对相同值返回相同引用。相等性检查从 O(n) 的值比较变成 O(1) 的指针比较。
flowchart LR
A["请求 'hello'"] --> P[享元池]
B["请求 'hello'"] --> P
C["请求 'world'"] --> P
P -->|"相同引用"| H["'hello'\n1 个实例"]
P -->|"相同引用"| H
P -->|"新建"| W["'world'\n1 个实例"]
| 属性 | 值 | 说明 |
|---|---|---|
| 查找/驻留 | O(1) 摊销 | 哈希表查找 |
| 内存节省 | O(唯一值) | 而非 O(总实例数) |
| 相等性检查 | O(1) | 指针比较代替值比较 |
| 权衡 | 池内存 + 查找成本 | vs 逐对象分配成本 |
享元的不可变性要求:如果一个驻留对象被修改,所有共享该引用的消费者都会受到影响。享元对象必须是不可变的。需要修改时,使用写时复制或不驻留。
四、变体与对比
| 模式 | 与享元的关系 | 适用场景 |
|---|---|---|
| 驻留(Interning) | 驻留是实现享元的机制——去重相同的值 | 字符串和符号去重 |
| 写时复制(CoW) | 享元共享不可变对象;CoW 共享直到变更 | 读多写少但偶尔需要修改 |
| LRU 缓存 | LRU 缓存可以淘汰最少使用的共享享元实例 | 内存受限的享元池 |
| 对象池 | 对象池复用可变对象;享元共享不可变对象 | 高频创建销毁 vs 长期共享 |
五、多语言实现
5.1 Go 实现
package flyweight
import "sync"
// TextStyle 文本格式享元——不可变type TextStyle struct { Font string Size int Color string}
// textStyleKey 用作享元池的键type textStyleKey struct { Font string Size int Color string}
// StyleFactory 享元工厂,确保相同格式只创建一份type StyleFactory struct { mu sync.RWMutex pool map[textStyleKey]*TextStyle}
func NewStyleFactory() *StyleFactory { return &StyleFactory{ pool: make(map[textStyleKey]*TextStyle), }}
// GetStyle 获取享元对象,相同参数返回同一指针func (f *StyleFactory) GetStyle(font string, size int, color string) *TextStyle { key := textStyleKey{Font: font, Size: size, Color: color}
f.mu.RLock() if style, ok := f.pool[key]; ok { f.mu.RUnlock() return style // 命中:返回已有实例 } f.mu.RUnlock()
// 未命中:创建新实例 f.mu.Lock() defer f.mu.Unlock()
// 双重检查,防止并发创建 if style, ok := f.pool[key]; ok { return style }
style := &TextStyle{Font: font, Size: size, Color: color} f.pool[key] = style return style}
// Character 字符对象——外部状态独立,内部状态共享type Character struct { Char rune X, Y float64 // 外部状态:位置 Style *TextStyle // 内部状态:共享的享元}5.2 TypeScript 实现
// 享元工厂:确保相同值只创建一份class FlyweightFactory<T> { private pool = new Map<string, T>();
// 获取或创建享元对象 intern(key: string, create: () => T): T { if (this.pool.has(key)) { return this.pool.get(key)!; } const value = create(); this.pool.set(key, value); return value; }
has(key: string): boolean { return this.pool.has(key); }
get size(): number { return this.pool.size; }}
// 使用示例:游戏引擎中的树木渲染interface TreeModel { mesh: string; texture: string;}
interface Tree { x: number; y: number; health: number; model: TreeModel; // 享元:所有同类树共享同一个模型}
const modelFactory = new FlyweightFactory<TreeModel>();
// 1000 棵松树共享同一个模型对象const pineModel = modelFactory.intern("pine", () => ({ mesh: "pine.mesh", texture: "pine.texture",}));
const trees: Tree[] = Array.from({ length: 1000 }, (_, i) => ({ x: Math.random() * 1000, y: Math.random() * 1000, health: 100, model: pineModel, // 全部指向同一个对象}));
// 相等性检查:指针比较即可console.log(trees[0].model === trees[999].model); // true六、生产验证
| 项目 | 源码位置 | 用途 |
|---|---|---|
| CPython | longobject.c#L61-L75 | get_small_int 返回 -5 到 256 的预缓存整数对象。a = 42; b = 42; a is b 为 True,这是享元模式 |
| Java String.intern() | JVM 字符串池 | JVM 自动驻留所有字符串字面量,相同字面量指向同一个 String 对象 |
| Chromium CSS | Blink 渲染引擎 | Blink 中 CSS 值去重,相同颜色、字体等样式值共享同一对象 |
七、小结
何时使用:
- 重复相同值——字符串、颜色、类型标签、枚举值
- 内存受限环境——嵌入式系统、移动端、浏览器标签页
- 编译器/解释器——符号表、关键字、类型描述符
- 游戏引擎——共享网格、纹理、材质
何时不用:
- 值全部唯一——池增加查找开销,没有节省
- 可变对象——享元假设共享对象不可变,修改会影响所有引用方
- 短生命周期数据——快速创建和丢弃的对象,驻留开销超过收益
- 线程安全要求高——并发驻留需要同步机制,可能成为瓶颈
八、参考资料
- CPython 小整数缓存 - Python 预缓存 -5 到 256 的整数对象
- Java String.intern() - JVM 字符串池的驻留机制
- V8 Internalized Strings - V8 引擎驻留属性名字符串以实现 O(1) 属性查找
- Rust string_cache crate - Servo 浏览器引擎中的 CSS/HTML 标签驻留
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时
相关文章 智能推荐
1
对象池(Object Pool)
程序设计 预分配一组对象反复使用,避免频繁创建和销毁带来的 GC 压力——游戏引擎、网络框架和高性能服务中的经典优化手段。
2
Arena 分配器(Arena Allocator)
程序设计 整块申请内存,用完一次性释放——bump pointer 分配的极致性能,编译器和游戏引擎的核心内存策略。
3
写时复制(Copy-on-Write)
程序设计 共享读取,只在修改时才复制——节省内存的延迟优化策略,Linux fork 和 Git 对象模型的基石。
4
空闲链表(Free List)
程序设计 用链表管理已释放的内存块,O(1) 分配和释放——操作系统内核和游戏引擎的底层分配利器。
5
驻留(Interning)
程序设计 值相同的对象只保留一份——字符串和符号去重的经典技术,用 O(1) 的指针比较替代 O(n) 的内容比较。






