mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1092 字
3 分钟
享元(Flyweight)
2026-06-13

一、为什么需要享元#

一个文本编辑器打开了一份 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

六、生产验证#

项目源码位置用途
CPythonlongobject.c#L61-L75get_small_int 返回 -5 到 256 的预缓存整数对象。a = 42; b = 42; a is bTrue,这是享元模式
Java String.intern()JVM 字符串池JVM 自动驻留所有字符串字面量,相同字面量指向同一个 String 对象
Chromium CSSBlink 渲染引擎Blink 中 CSS 值去重,相同颜色、字体等样式值共享同一对象

七、小结#

何时使用:

  • 重复相同值——字符串、颜色、类型标签、枚举值
  • 内存受限环境——嵌入式系统、移动端、浏览器标签页
  • 编译器/解释器——符号表、关键字、类型描述符
  • 游戏引擎——共享网格、纹理、材质

何时不用:

  • 值全部唯一——池增加查找开销,没有节省
  • 可变对象——享元假设共享对象不可变,修改会影响所有引用方
  • 短生命周期数据——快速创建和丢弃的对象,驻留开销超过收益
  • 线程安全要求高——并发驻留需要同步机制,可能成为瓶颈

八、参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

享元(Flyweight)
https://blog.souloss.com/posts/programming/memory/memory-flyweight/
作者
Tsukimi
发布于
2026-06-13
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时