设计模式 · 享元模式(Flyweight Pattern)java

前言

在实际开发中,我们经常会遇到一种情况:系统里有大量对象,但这些对象中很多数据是重复的

比如:

1
2
3
4
5
游戏地图中有 10 万棵树:
每棵树都有坐标、颜色、纹理、树种。

秒杀系统中有很多活动查询:
每次查询都有活动名称、活动描述、开始时间、结束时间、库存信息。

随着业务规模的扩张,这些海量的对象会像无底洞一样吞噬系统的内存,导致频繁的垃圾回收(GC),甚至引发系统崩溃(OOM)

对于这种情况,如何用最少的资源办最多的事?

如果每次都创建一个完整的对象,就像每个学生都要一本完全一样的教材,但学校却给每个人重新打印一遍,这显然会浪费大量资源。

共享元模式就是为了解决这种“重复对象过多,内存浪费严重”的问题。

其核心思想是:

相同的内容共享,不同的外部内容形成。

本文代码:https://github.com/likerhood/CodeDesignWork/tree/main/codedesign10.0-0 和 10.0-1


一、核心定义

享元模式的核心思想非常纯粹:运用共享技术,有效地支持大量细粒度对象的复用

它的定义可以这样理解:

通过共享已经存在的对象,减少大量相似对象的创建,从而降低内存消耗,提高系统访问效率。

更直白地说:

  1. 不要每次都 new 一个完整对象;
  2. 能共享的部分就共享;
  3. 不能共享的部分再单独传入;

这里的思想和原型模式比较接近。

在共享元模式中,最重要的是区分两个状态:

状态 意义 是否共享 举例说明
内在状态 对象中稳定、不变、可复用的数据 可以分享 活动名称、活动描述、树的纹理、树的颜色
外部在状态 每个对象突出、经常变化的数据 不分享 库存数量、树的坐标、当前用户状态

类比在购物节的秒似杀活动中:

  1. 活动ID、活动名称、活动描述、开始时间、结束时间,这些通常是不变的,可以共享。

  2. 库存数量、已售数量、用户下单状态,这些是变化的,不适合共享。

所以享元模式的本质是:完整对象 = 共享对象 + 外部状态


二、标准体系结构图

享元模式的标准体系通常包含以下核心角色:

  1. FlyweightFactory(享元工厂):负责创建和管理享元对象。当客户端请求时,工厂会检查池中是否已有符合要求的对象,如果有则直接返回,没有则创建新对象并放入池中。
  2. Flyweight(抽象享元接口):规定了具体享元类必须实现的方法,通常会接收外部状态作为参数。
  3. ConcreteFlyweight(具体享元类):实现了接口,并为内部状态提供存储空间。

这里的重点在于:

  1. 客户端不直接创建共享对象;
  2. 客户端通过享元工厂获取共享对象;
  3. 享元工厂内部维护一个对象池;
    • 如果对象已经存在,直接复用;
    • 如果对象不存在,再创建并放入对象池。

三、场景推演

在游戏开发中,通常需要渲染成千上万的树木来构成一片广袤的森林。如果每一棵树都包含完整的品种信息、高分辨率的树皮贴图模型(几十MB大小)以及具体的坐标位置,一百万棵树就会直接撑爆显存。

但仔细推演我们会发现:森林里的树虽然多,但“品种”其实只有寥寥几种(比如橡树、松树、白桦树)。树的品种和贴图模型是永远不变的(内部状态),而树在地图上的 X、Y 坐标是每棵树独有的(外部状态)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

// 享元对象:树的类型,保存可共享的内部状态
class TreeType {

// 内部状态:树的品种
private final String name;

// 内部状态:树皮贴图
private final String barkTexture;

// 内部状态:树的模型数据
private final String modelData;

public TreeType(String name, String barkTexture, String modelData) {
this.name = name;
this.barkTexture = barkTexture;
this.modelData = modelData;
}

// 外部状态通过参数传入
public void render(int x, int y) {
System.out.println(
"渲染树木:" + name +
",坐标:(" + x + ", " + y + ")" +
",贴图:" + barkTexture +
",模型:" + modelData
);
}
}

// 享元工厂:负责创建和复用 TreeType
class TreeFactory {

private static final Map<String, TreeType> TREE_TYPE_POOL = new HashMap<>();

public static TreeType getTreeType(String name, String barkTexture, String modelData) {
String key = name + "_" + barkTexture + "_" + modelData;

if (!TREE_TYPE_POOL.containsKey(key)) {
System.out.println("创建新的树类型:" + name);
TREE_TYPE_POOL.put(key, new TreeType(name, barkTexture, modelData));
}

return TREE_TYPE_POOL.get(key);
}

public static int getTreeTypeCount() {
return TREE_TYPE_POOL.size();
}
}

// 具体树对象:保存每棵树独有的外部状态
class Tree {

// 外部状态:每棵树的位置不同
private final int x;
private final int y;

// 引用共享的树类型对象
private final TreeType treeType;

public Tree(int x, int y, TreeType treeType) {
this.x = x;
this.y = y;
this.treeType = treeType;
}

public void render() {
treeType.render(x, y);
}
}

// 森林类:管理大量树木
class Forest {

private final List<Tree> trees = new ArrayList<>();

public void plantTree(int x, int y, String name, String barkTexture, String modelData) {
TreeType treeType = TreeFactory.getTreeType(name, barkTexture, modelData);
Tree tree = new Tree(x, y, treeType);
trees.add(tree);
}

public void render() {
for (Tree tree : trees) {
tree.render();
}
}

public int getTreeCount() {
return trees.size();
}
}

// 测试类
public class FlyweightTreeDemo {

public static void main(String[] args) {
Forest forest = new Forest();

// 种植很多棵橡树
forest.plantTree(10, 20, "橡树", "oak_bark_texture.png", "oak_model.obj");
forest.plantTree(30, 50, "橡树", "oak_bark_texture.png", "oak_model.obj");
forest.plantTree(80, 120, "橡树", "oak_bark_texture.png", "oak_model.obj");

// 种植很多棵松树
forest.plantTree(200, 300, "松树", "pine_bark_texture.png", "pine_model.obj");
forest.plantTree(250, 360, "松树", "pine_bark_texture.png", "pine_model.obj");

// 种植白桦树
forest.plantTree(400, 500, "白桦树", "birch_bark_texture.png", "birch_model.obj");

forest.render();

System.out.println("----------------------");
System.out.println("森林中的树木总数:" + forest.getTreeCount());
System.out.println("实际创建的树类型数量:" + TreeFactory.getTreeTypeCount());
}
}

最核心的地方是这句:

1
TreeType treeType = TreeFactory.getTreeType(name, barkTexture, modelData);

它表示:

种树时不要每次都创建完整树模型,而是先去享元池里找有没有相同类型的树。如果有,就直接复用。

这些数据不再每棵树都复制一份,而是相同类型的树共享一份。

Tree 对象还是创建了 6 个,但 TreeType 这种重量级、可共享的对象只创建了 3 个。


四、实战案例

4.1 需求分析

秒杀活动查询接口通常会返回活动 ID、活动名称、活动描述、开始时间、结束时间、库存等信息。

普通实现中,每次请求都会重新创建完整的活动对象,活动基础信息和库存信息一起被反复构建。

但在真实秒杀场景中,活动名称、描述、开始/结束时间等属于相对稳定的内部状态,而库存已用数量属于频繁变化的外部状态。

享元模式的核心思路就是:复用稳定对象,只把变化数据放在外部动态补充,从而减少对象创建和内存占用。

4.2 架构图

4.2.1 普通方式架构图

image-20260520220232223

4.2.2 享元模式架构图

image-20260520221249113

4.3 时序图

4.3.1 普通代码时序图

4.3.2 享元模式时序图

4.4 代码分析

4.4.1 普通代码

普通实现的核心问题在于每次请求都会重新创建 Activity,并把活动基础信息、活动时间、库存信息全部硬编码到接口逻辑里。

1
2
3
4
5
6
7
8
9
10
public Activity queryActivityInfo(Long id) {
Activity activity = new Activity();
activity.setId(10001L);
activity.setName("图书嗨乐");
activity.setDesc("图书优惠券分享激励分享活动第二期");
activity.setStartTime(new Date());
activity.setStopTime(new Date());
activity.setStock(new Stock(1000, 1));
return activity;
}

这种方式简单直接,但当请求量变大时,大量重复对象会被创建。

活动基础信息本身并不会频繁变化,却在每一次查询中重新构造,造成不必要的内存和对象创建成本。

4.4.2 享元模式代码

享元模式版本引入 ActivityFactory,通过 Map<Long, Activity> 缓存活动对象。

活动 ID、名称、描述、时间等稳定信息作为内部状态被共享;库存已用数量作为外部状态,从 RedisUtils 中动态读取后重新设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class ActivityFactory {

static Map<Long, Activity> activityMap = new HashMap<>();

public static Activity getActivity(Long id) {
Activity activity = activityMap.get(id);
if (null == activity) {
activity = new Activity();
activity.setId(10001L);
activity.setName("图书嗨乐");
activity.setDesc("图书优惠券分享激励分享活动第二期");
activity.setStartTime(new Date());
activity.setStopTime(new Date());
activityMap.put(id, activity);
}
return activity;
}
}

public Activity queryActivityInfo(Long id) {
Activity activity = ActivityFactory.getActivity(id);
Stock stock = new Stock(1000, redisUtils.getStockUsed());
activity.setStock(stock);
return activity;
}

整体来看,享元模式把“不变的活动信息”和“变化的库存信息”拆开处理:前者缓存复用,后者按请求动态补充。这样可以减少重复对象创建,也让业务结构更贴合高并发秒杀查询场景。

总结

享元模式是空间换时间(或共享换空间)的经典体现。通过构建缓存池和状态拆分,它能在高并发、大数据量的场景下,将内存占用成千上万倍地压缩。

虽然它极大提升了性能,但也引入了状态分离的系统复杂度。

在实际应用中(如 Java 的 String 常量池、数据库连接池、线程池),我们需要时刻警惕多线程环境下的线程安全问题,确保享元对象的“内部状态”绝对不可被篡改。