文章目录
-
- 前言
- 一、核心定义
- 二、标准体系结构图
- 三、场景推演
- 四、实战案例
-
- [4.1 需求分析](#4.1 需求分析)
- [4.2 架构图](#4.2 架构图)
-
- [4.2.1 普通方式架构图](#4.2.1 普通方式架构图)
- [4.2.2 享元模式架构图](#4.2.2 享元模式架构图)
- [4.3 时序图](#4.3 时序图)
-
- [4.3.1 普通代码时序图](#4.3.1 普通代码时序图)
- [4.3.2 享元模式时序图](#4.3.2 享元模式时序图)
- [4.4 代码分析](#4.4 代码分析)
-
- [4.4.1 普通代码](#4.4.1 普通代码)
- [4.4.2 享元模式代码](#4.4.2 享元模式代码)
- 总结
前言
在实际开发中,我们经常会遇到一种情况:系统里有大量对象,但这些对象中很多数据是重复的。
比如:
游戏地图中有 10 万棵树:
每棵树都有坐标、颜色、纹理、树种。
秒杀系统中有很多活动查询:
每次查询都有活动名称、活动描述、开始时间、结束时间、库存信息。
随着业务规模的扩张,这些海量的对象会像无底洞一样吞噬系统的内存,导致频繁的垃圾回收(GC),甚至引发系统崩溃(OOM)。
对于这种情况,如何用最少的资源办最多的事?
如果每次都创建一个完整的对象,就像每个学生都要一本完全一样的教材,但学校却给每个人重新打印一遍,这显然会浪费大量资源。
共享元模式就是为了解决这种"重复对象过多,内存浪费严重"的问题。
其核心思想是:
相同的内容共享,不同的外部内容形成。
本文代码:https://github.com/likerhood/CodeDesignWork/tree/main/codedesign10.0-0 和 10.0-1
一、核心定义
享元模式的核心思想非常纯粹:运用共享技术,有效地支持大量细粒度对象的复用。
它的定义可以这样理解:
通过共享已经存在的对象,减少大量相似对象的创建,从而降低内存消耗,提高系统访问效率。
更直白地说:
- 不要每次都 new 一个完整对象;
- 能共享的部分就共享;
- 不能共享的部分再单独传入;
这里的思想和原型模式比较接近。
在共享元模式中,最重要的是区分两个状态:
| 状态 | 意义 | 是否共享 | 举例说明 |
|---|---|---|---|
| 内在状态 | 对象中稳定、不变、可复用的数据 | 可以分享 | 活动名称、活动描述、树的纹理、树的颜色 |
| 外部在状态 | 每个对象突出、经常变化的数据 | 不分享 | 库存数量、树的坐标、当前用户状态 |
类比在购物节的秒似杀活动中:
-
活动ID、活动名称、活动描述、开始时间、结束时间,这些通常是不变的,可以共享。
-
库存数量、已售数量、用户下单状态,这些是变化的,不适合共享。
所以享元模式的本质是:完整对象 = 共享对象 + 外部状态
二、标准体系结构图
享元模式的标准体系通常包含以下核心角色:
- FlyweightFactory(享元工厂):负责创建和管理享元对象。当客户端请求时,工厂会检查池中是否已有符合要求的对象,如果有则直接返回,没有则创建新对象并放入池中。
- Flyweight(抽象享元接口):规定了具体享元类必须实现的方法,通常会接收外部状态作为参数。
- ConcreteFlyweight(具体享元类):实现了接口,并为内部状态提供存储空间。
这里的重点在于:
- 客户端不直接创建共享对象;
- 客户端通过享元工厂获取共享对象;
- 享元工厂内部维护一个对象池;
- 如果对象已经存在,直接复用;
- 如果对象不存在,再创建并放入对象池。
获取享元对象
管理对象池
Client
+operation()
FlyweightFactory
-Map<String, Flyweight> pool
+getFlyweight(key) : Flyweight
<<interface>>
Flyweight
+operation(extrinsicState)
ConcreteFlyweight
-intrinsicState
+operation(extrinsicState)
三、场景推演
在游戏开发中,通常需要渲染成千上万的树木来构成一片广袤的森林。如果每一棵树都包含完整的品种信息、高分辨率的树皮贴图模型(几十MB大小)以及具体的坐标位置,一百万棵树就会直接撑爆显存。
但仔细推演我们会发现:森林里的树虽然多,但"品种"其实只有寥寥几种(比如橡树、松树、白桦树)。树的品种和贴图模型是永远不变的(内部状态) ,而树在地图上的 X、Y 坐标是每棵树独有的(外部状态)。
JAVA
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());
}
}
最核心的地方是这句:
java
TreeType treeType = TreeFactory.getTreeType(name, barkTexture, modelData);
它表示:
种树时不要每次都创建完整树模型,而是先去享元池里找有没有相同类型的树。如果有,就直接复用。
这些数据不再每棵树都复制一份,而是相同类型的树共享一份。
Tree 对象还是创建了 6 个,但 TreeType 这种重量级、可共享的对象只创建了 3 个。
四、实战案例
4.1 需求分析
秒杀活动查询接口通常会返回活动 ID、活动名称、活动描述、开始时间、结束时间、库存等信息。
普通实现中,每次请求都会重新创建完整的活动对象,活动基础信息和库存信息一起被反复构建。
但在真实秒杀场景中,活动名称、描述、开始/结束时间等属于相对稳定的内部状态,而库存已用数量属于频繁变化的外部状态。
享元模式的核心思路就是:复用稳定对象,只把变化数据放在外部动态补充,从而减少对象创建和内存占用。
4.2 架构图
4.2.1 普通方式架构图

4.2.2 享元模式架构图

4.3 时序图
4.3.1 普通代码时序图
Stock Activity ActivityController ApiTest Stock Activity ActivityController ApiTest queryActivityInfo(10001L) new Activity() setId/setName/setDesc/setTime new Stock(1000, 1) setStock(stock) return Activity
4.3.2 享元模式时序图
Stock RedisUtils Shared Activity ActivityFactory ActivityController ApiTest Stock RedisUtils Shared Activity ActivityFactory ActivityController ApiTest alt [缓存不存在] [缓存存在] queryActivityInfo(10001L) getActivity(10001L) new Activity() activityMap.put(id, activity) return cached Activity getStockUsed() used new Stock(1000, used) setStock(stock) return Activity
4.4 代码分析
4.4.1 普通代码
普通实现的核心问题在于每次请求都会重新创建 Activity,并把活动基础信息、活动时间、库存信息全部硬编码到接口逻辑里。
java
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 中动态读取后重新设置。
java
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 常量池、数据库连接池、线程池),我们需要时刻警惕多线程环境下的线程安全问题,确保享元对象的"内部状态"绝对不可被篡改。