news 2026/7/18 5:03:36

走进 Gang of Four 设计模式:享元模式

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
走进 Gang of Four 设计模式:享元模式

走进 Gang of Four 设计模式:享元模式

说明:不仅告诉你"怎么用",更告诉你"为什么这样设计"

前置知识:Java 面向对象基础(接口、HashMap、不可变对象)、内存管理基本概念


📊 设计模式分类总览

GoF 23 种设计模式可按两个维度交叉分类:类/对象(处理方式) ×创建型/结构型/行为型(目的)。

维度创建型(Creational)结构型(Structural)行为型(Behavioral)
类(Class)
(通过继承复用)
Factory Method
工厂方法
Adapter(类适配器)Interpreter(解释器)
Template Method(模板方法)
对象(Object)
(通过组合/聚合复用)
Abstract Factory(抽象工厂)
Builder(建造者)
Prototype(原型)
Singleton(单例)
Adapter(对象适配器)
Bridge(桥接)
Composite(组合)
Decorator(装饰)
Facade(外观)
Flyweight(享元)
Proxy(代理)
Chain of Resp.(责任链)
Command(命令)
Iterator(迭代器)
Mediator(中介者)
Memento(备忘录)
Observer(观察者)
State(状态)
Strategy(策略)
Visitor(访问者)

📑 目录

  1. 模式概述 · 15 问深度分析
  2. 框架源码实战分析
    • JDK Integer.valueOf 与 String 常量池
    • Spring 单例 Bean 容器
    • MyBatis SqlNode 共享元数据
    • Tomcat StringCache
  3. 深度追问:Why / How / Trade-off / Evolution / Modern Practice
  4. 总结
  5. 参考文献

1. 模式概述 · 15 问深度分析

Q1: 为什么需要这个模式?它解决了什么问题?

在一些应用中,可能会出现需要创建海量相似对象的情况(例如:文本编辑器中的每一个字符、游戏中的成千上万个小兵、地图上的每一个建筑图标)。

  • 解决的问题:解决了由于大量对象的创建而导致的内存溢出(OOM)或内存开销过大、GC 过于频繁导致系统卡顿的问题。
  • 根本目的:通过共享技术实现相同或相似对象的重用,大幅减少内存中对象的数量。

Q2: 如果不用这个模式,会有什么缺陷?

  • 内存暴涨:如果每一个微小的实例都独立占用内存,内存会迅速被吃光。
  • GC 频繁:生命周期短暂的大量对象会频繁触发 JVM 的 Young GC 甚至 Full GC,导致系统出现明显的 STW 卡顿。
  • 创建开销大:频繁地new对象并进行初始化,消耗大量 CPU 资源和时间。

Q3: 核心思想是什么?一句话如何概括?

  • 核心思想:将对象的状态划分为内部状态(Internal State)外部状态(External State)。内部状态可共享,外部状态由客户端传入。
  • 一句话概括运用共享技术有效地支持大量细粒度的对象。

Q4: 包含哪些角色?每个角色的职责是什么?

角色职责
Flyweight(抽象享元)定义产品的抽象接口,包含需要外部状态的方法
ConcreteFlyweight(具体享元)实现抽象接口,保存内部状态(不可变、可共享)
FlyweightFactory(享元工厂)核心角色,管理享元对象池,检查复用或创建
Client(客户端)维护外部状态,调用时传入享元方法

Q5: 它们之间如何协作?调用流程是怎样的?

  1. 客户端需要对象时,不直接new,而是向FlyweightFactory请求,传入标识(Key)。
  2. FlyweightFactory检查缓存中是否存在该 Key:
    • 如果有,直接返回。
    • 如果没有,创建一个新的ConcreteFlyweight存入缓存,然后返回。
  3. 客户端获取享元对象后,在调用其方法时将外部状态作为参数传递进去。享元对象结合自身内部状态和传入的外部状态完成业务逻辑。

Q6: 为什么要这样设计?每个角色存在的意义是什么?

  • 把外部状态剥离出来:这是享元模式能成功缩减对象的关键。如果对象自己持有所有状态,它就无法被别人复用。
  • 工厂(Factory)存在的意义:它是"共享"的控制中心。没有工厂统一管理,客户端各自new对象,共享就无从谈起。
  • 具体享元存在的意义:它是真正的业务承载者,只保存"不管在什么环境下都不变"的硬核心数据(内部状态)。

Q7: 为什么使用接口、抽象类、组合,而不是其他方式?

  • 为什么用接口/抽象类:为了多态性。工厂返回统一的抽象享元类型,客户端不需要知道具体拿到的是哪一个共享实例。
  • 为什么用组合(通过参数传入外部状态):外部状态是多变的,如果使用继承,每多一种外部状态就要派生一个子类,导致类爆炸。通过组合/参数传递,把外部状态变成行为的"入参",完美实现解耦。

Q8: 符合哪些面向对象原则?

  • 单一职责原则(SRP):享元对象只负责维护内部状态和核心逻辑,工厂只负责对象的生命周期和缓存管理。
  • 开闭原则(OCP):增加新的具体享元类型,只需扩展抽象享元并让工厂支持即可。
  • 面向接口编程(DIP):客户端依赖的是Flyweight接口,而不是具体的实现类。

Q9: 优缺点与额外成本

优点:

  • 极大减少内存中对象的数量,降低 OOM 风险。
  • 外部状态独立,不会相互影响。

局限性/额外成本:

  • 系统复杂度显著提高:需要分离内部状态和外部状态,增加了代码理解成本。
  • 运行时间成本:每次读取/计算外部状态都需要通过参数传递,属于"用时间换空间"。

Q10: 最适合/不该用的场景

最适合:

  • 系统中有大量相同或相似的对象,导致内存开销巨大。
  • 对象的大多数状态都可以变为外部状态。
  • 典型的如:围棋/象棋游戏、文本渲染引擎、地图 POI 标记。

不应该使用:

  • 对象数量很少,或对象之间几乎没有任何共同状态。
  • 内存绰绰有余,而系统对执行速度有极高的要求。

Q11: 与其他模式的区别

与单例模式的区别:

  • 单例模式是全局只有一个实例
  • 享元模式在工厂里管理着一组/多个不同的对象实例。

与原型模式的区别:

  • 原型模式是通过克隆已有的对象来创建全新的独立对象(不共享内存)。
  • 享元模式是为了复用同一个对象,减少创建。

与组合模式的联系:

  • 享元模式经常和组合模式结合使用。组合模式中的叶子节点如果数量巨大且类似,可以设计为享元对象。

Q12: 框架中的典型应用

  • JDKInteger.valueOf(int i)内部有IntegerCache,缓存 -128 到 127 之间的 Integer 对象。String常量池也是极典型的享元应用。
  • Spring:Spring 的 Bean 容器(默认 Singleton 作用域)管理着各种单例 Bean,通过beanName在 Map 中复用对象。
  • MyBatisSqlNode的共享元数据,SQL 解析后的结构树被缓存复用。
  • TomcatStringCache(字符串缓存),复用常用的 HTTP 头部、方法名等字符串。

Q13: 最小可运行版本(五子棋棋子)

importjava.util.HashMap;importjava.util.Map;// 1. 抽象享元interfaceChessPiece{voiddraw(intx,inty);// x, y 是外部状态}// 2. 具体享元(只包含内部状态)classConcreteChessPieceimplementsChessPiece{privatefinalStringcolor;// 内部状态:颜色publicConcreteChessPiece(Stringcolor){this.color=color;}@Overridepublicvoiddraw(intx,inty){System.out.println("在坐标 ("+x+", "+y+") 放置了一颗 ["+color+"] 棋子");}}// 3. 享元工厂classChessPieceFactory{privatestaticfinalMap<String,ChessPiece>pool=newHashMap<>();publicstaticChessPiecegetChessPiece(Stringcolor){if(!pool.containsKey(color)){pool.put(color,newConcreteChessPiece(color));System.out.println("== 创建了新的 ["+color+"] 棋子对象 ==");}returnpool.get(color);}}// 4. 客户端publicclassMain{publicstaticvoidmain(String[]args){ChessPieceblack1=ChessPieceFactory.getChessPiece("黑色");black1.draw(1,1);ChessPiecewhite1=ChessPieceFactory.getChessPiece("白色");white1.draw(2,2);ChessPieceblack2=ChessPieceFactory.getChessPiece("黑色");black2.draw(3,3);ChessPiecewhite2=ChessPieceFactory.getChessPiece("白色");white2.draw(4,4);System.out.println("black1 == black2: "+(black1==black2));// true}}

Q14: 识别重构信号

  1. 内存监控异常:通过 Profiler 发现某种简单对象的实例数达到数十万甚至数百万,占用了大部分堆内存。
  2. 代码中存在大量重复数据:发现这些对象中,有 80% 的属性字段全都是一模一样的。

Q15: 现代改进

Java 16+ Record:public record ChessPiece(String color) { public void draw(int x, int y) { ... } }

函数式工厂(ComputeIfAbsent):

publicclassChessFactory{privatestaticfinalMap<String,ChessPiece>pool=newConcurrentHashMap<>();publicstaticChessPieceget(Stringcolor){returnpool.computeIfAbsent(color,ConcreteChessPiece::new);}}

微服务/分布式视角:在微服务架构中,单机的享元扩展成了分布式缓存/元数据服务。海量订单中重复的"商品详情静态元数据",通过 Redis 缓存或在 BFF 层进行数据聚合拉取,核心思想依然是"剥离外部动态状态,共享静态内部状态"。


2. 框架源码实战分析

2.1 JDK Integer.valueOf 与 String 常量池

Integer.valueOf 源码
publicstaticIntegervalueOf(inti){if(i>=IntegerCache.low&&i<=IntegerCache.high)returnIntegerCache.cache[i+(-IntegerCache.low)];returnnewInteger(i);}

IntegerCache 享元池:

privatestaticclassIntegerCache{staticfinalintlow=-128;staticfinalinthigh;staticfinalInteger[]cache;static{inth=127;high=h;cache=newInteger[(high-low)+1];intj=low;for(intk=0;k<cache.length;k++)cache[k]=newInteger(j++);}}
  • 内部状态(共享)Integervalue属性(private final int value),天然不可变,线程安全。
  • 外部状态:无(属于单纯享元模式,核心在于用空间换取高频数字的创建时间与内存开销)。
String 常量池

Java 的String常量池(StringTable)在 JVM 层面是一个固定大小的 HashTable。使用字面量String s = "abc"时,JVM 先去StringTable检查是否存在值为"abc"的引用。存在则直接返回,不存在则创建并存入。

2.2 Spring 单例 Bean 容器

Spring 的单例 Bean 容器从设计初衷上看,其核心逻辑与享元工厂非常相似。

核心源码——DefaultSingletonBeanRegistry

/** Cache of singleton objects: bean name to bean instance. */privatefinalMap<String,Object>singletonObjects=newConcurrentHashMap<>(256);

AbstractBeanFactory.doGetBean获取单例 Bean:

protected<T>TdoGetBean(Stringname,@NullableClass<T>requiredType,...){// 1. 尝试从单例享元池中直接获取ObjectsharedInstance=getSingleton(beanName);if(sharedInstance!=null&&args==null){bean=getObjectForBeanInstance(sharedInstance,name,beanName,null);}else{// 2. 享元池里没有,创建并缓存if(isSingleton(beanName)){sharedInstance=getSingleton(beanName,()->createBean(beanName,mbd,args));bean=getObjectForBeanInstance(sharedInstance,name,beanName,mbd);}}return(T)bean;}
  • 内部状态(共享):Bean 自身的类结构、无状态的方法、无状态的配置属性。
  • 外部状态(非共享):每一次具体业务请求传入的参数。
  • 为什么说是变形?享元模式处理大量细粒度对象,Spring Bean 通常是大粒度的。但从"通过beanName去注册表中获取并复用无状态对象,避免重复创建"的角度来看,两者的底层逻辑完全一致。

2.3 MyBatis SqlNode 共享元数据

MyBatis 的 XML 动态标签在解析后被转换成SqlNode树,这些节点被一次性构建并缓存起来。当多个线程高并发执行同一条动态 SQL 时,大家复用同一个IfSqlNode实例,只需传入各自不同的DynamicContext

publicclassIfSqlNodeimplementsSqlNode{privatefinalExpressionEvaluatorevaluator;privatefinalStringtest;// 内部状态:表达式文本privatefinalSqlNodecontents;// 内部状态:子 SQL 节点@Overridepublicbooleanapply(DynamicContextcontext){// context 是外部状态:包含了本次调用的实际入参if(evaluator.evaluateBoolean(test,context.getBindings())){contents.apply(context);returntrue;}returnfalse;}}
  • 内部状态(共享)test表达式字符串、contents子节点。XML 加载时确定,无论谁调用都不会变。
  • 外部状态(通过参数传入)DynamicContext context,承载本次请求的实际入参和StringBuilder

2.4 Tomcat StringCache

高并发 HTTP 请求带来大量重复字符串(GETPOSTHTTP/1.1Host)。Tomcat 设计了StringCache来复用这些字符串。

核心逻辑(简化版):

publicclassStringCache{protectedstaticByteEntry[][]bcCache=null;protectedstaticCharEntry[][]ccCache=null;publicstaticStringtoString(ByteChunkbc){if(!cacheEnabled){returnbc.toStringInternal();// 没开启缓存,直接 new String}inthash=bc.getHash();intindex=findClosest(bc,bcCache[hash&accessCount]);if(index>=0){returnbcCache[hash&accessCount][index].name;// 命中缓存,直接返回}else{Stringvalue=bc.toStringInternal();// 高频访问才加入享元池returnvalue;}}}
  • 内部状态ByteEntry.name(已经转换好的String对象)。
  • 外部状态:网络中源源不断流进来的原始 TCP 字节流(ByteChunk)。
  • 应用价值:Tomcat 每秒处理数万次请求,Connection: keep-aliveAccept-Language等常见 HTTP 头文本在内存中只保留极少数的单例,其余全部复用引用,对压榨 JVM 内存、减少 GC 停顿起到决定性作用。

四者对比总结:

框架/组件享元工厂享元池内部状态外部状态
JDK IntegerInteger类本身IntegerCache.cache数组-128~127int无(单纯享元)
SpringBeanFactorysingletonObjectsMapBean 方法体、静态配置业务方法的具体入参
MyBatisXMLScriptBuilderDynamicSqlSource树结构XML 静态文本和 OGNL 表达式DynamicContext参数绑定
TomcatStringCachebcCache/ccCache数组"GET","Host"等字符串原始 TCP 字节流ByteChunk

3. 深度追问

3.1 Why(为什么):解决哪个根本矛盾?

享元模式解决的是**"业务诉求上无限的细粒度实体对象"与"物理层面上有限的计算机内存空间"之间的根本矛盾。**

面向对象设计鼓励将世界拆解得越细越好(如文档编辑器里每个字符都应是对象),但物理硬件的内存是有限的。享元模式允许我们在逻辑上维持成千上万个独立的细粒度对象,但在物理上只占用极少量的内存。

3.2 How(怎么做):通过哪些对象关系和交互机制?

享元模式的核心机制是状态分离与参数化组合

A. 状态的彻底剥离:

  • 内部状态:存储在享元内部、不会随环境改变而改变的共享部分(如棋子的颜色),作为成员变量存在。
  • 外部状态:随环境改变而改变、无法共享的部分(如棋子的 X/Y 坐标),坚决不能作为成员变量

B. 行为的参数化交互:
客户端在调用享元对象的方法时,必须将外部状态通过方法参数传入。

3.3 Trade-off(代价):牺牲了什么换取什么?

换取(收益)牺牲(代价)
极大降低内存占用,规避 OOM运行时间成本:外部状态变成方法入参,需动态计算/查找
降低 GC 频率,系统更平稳代码内聚性:逻辑被撕裂在享元和客户端之间
系统复杂度:引入工厂、缓存、线程安全控制

3.4 Evolution(演化):如何逐步演化而来?

  1. 普通工厂:每次都返回新实例。
  2. 普通缓存:在工厂里加 Map 缓存,发现相同 Key 就返回同一个。
  3. 遇到瓶颈:缓存的对象持有特定环境数据,多线程并发时互相覆盖。
  4. 终极形态(Flyweight):强制把变动的数据从对象中剥离,只缓存纯净的核心。

衍生变体:

  • 单纯享元模式:所有享元对象都可共享。
  • 复合享元模式:多个单纯享元对象通过组合模式拼接而成,复合享元本身不能共享,但其内部子节点是共享的。

3.5 Modern Practice(现代实践):是否被取代?

被现代技术部分取代/弱化:

  • 分布式缓存(Redis):传统单机需要享元省内存,现在大型微服务中分布式缓存承担了"全局享元池"的角色。
  • 依赖注入容器:Spring IoC 容器本身就是对开发者透明的享元工厂,不需要自己写FlyweightFactory

与现代编程范式完美共生:

  • 函数式编程与不可变性:享元模式的内部状态要求"绝对不可变",与函数式编程理念一拍即合。Java 16+ 的record让编写不可变享元类变得极为简单。
  • 无状态架构:现代互联网高并发架构提倡"服务无状态"。API 网关、Controller/Service 的本质都是享元模式思想的无意识应用——对象只有一份(单例),高并发的 HTTP 请求(外部状态)作为参数流经这些无状态的服务节点。

4. 总结

核心要点

  • 享元模式通过共享对象减少内存消耗,核心机制是状态分离:内部状态(共享)由享元存储,外部状态(可变)由客户端传入
  • 与单例模式的区别:享元是一组对象,单例是一个对象
  • 四个工业级应用:IntegerCache(数字缓存)、SpringsingletonObjects(Bean 池)、MyBatisSqlNode(SQL 语义共享)、TomcatStringCache(HTTP 头部复用)
  • 现代演进:record不可变性、ConcurrentHashMap.computeIfAbsent、分布式 Redis 缓存

一句话

享元模式是"分门别类,同类共享"——把对象的相同部分抽出来共享,把不同部分交给客户端自己管理。


5. 参考文献

[1] GAMMA E, HELM R, JOHNSON R, et al. Design Patterns: Elements of Reusable Object-Oriented Software[M]. Boston: Addison-Wesley, 1994.
[2] Oracle. Java Integer / String Source Code[EB/OL]. https://docs.oracle.com/javase/8/docs/api/, 2024.
[3] SPRING. Spring BeanFactory[EB/OL]. https://docs.spring.io/spring-framework/, 2024.
[4] MyBatis. MyBatis 3 SqlNode[EB/OL]. https://github.com/mybatis/mybatis-3, 2024.
[5] Apache Tomcat. Tomcat StringCache[EB/OL]. https://tomcat.apache.org/, 2024.

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/18 5:03:30

大模型部署优化:从TPU硬件到MoE架构的系统工程实践

当4000亿参数的大模型遇上TPU v7x硬件平台&#xff0c;真正的挑战才刚刚开始。Google工程团队最近在Ironwood&#xff08;TPUv7&#xff09;平台上对Qwen 3.5-397B MoE模型进行的优化实践&#xff0c;揭示了一个关键趋势&#xff1a;大模型部署的瓶颈已经从算法设计转向了系统工…

作者头像 李华
网站建设 2026/7/18 5:03:17

C++实现导线网平差:从最小二乘法到误差椭圆分析

1. 项目概述&#xff1a;从测绘内业痛点出发 测绘工程专业的学生或者刚入行的技术人员&#xff0c;大概都经历过手算导线平差的“洗礼”。面对一沓观测数据&#xff0c;从角度闭合差计算到坐标增量改正&#xff0c;再到最终的精度评定&#xff0c;整个过程繁琐且极易出错。一个…

作者头像 李华
网站建设 2026/7/18 5:00:06

深入解析ADC数字比较器:硬件监控与实时响应的核心配置

1. ADC数字比较器&#xff1a;嵌入式实时监控的“哨兵”在嵌入式系统&#xff0c;尤其是实时控制领域&#xff0c;模数转换器&#xff08;ADC&#xff09;扮演着将物理世界连续变化的模拟信号&#xff08;如温度、压力、电压&#xff09;转换为微控制器可处理的离散数字值的“翻…

作者头像 李华
网站建设 2026/7/18 4:59:50

从零搭建NVIDIA Isaac Lab机器人学习环境:驱动、仿真与模型集成全攻略

1. 项目概述&#xff1a;从零构建你的机器人学习环境如果你正在机器人学习或仿真的领域摸索&#xff0c;尤其是想利用NVIDIA强大的Omniverse平台&#xff0c;那么“如何搭建NVIDIA Robotics Isaac Lab并添加机器人”这个标题&#xff0c;精准地指向了从环境部署到场景构建的核心…

作者头像 李华
网站建设 2026/7/18 4:59:28

阿里云 PolarDB vs 金仓 KingbaseES 国产数据库深度对比

在国产数据库选型中&#xff0c;阿里云 PolarDB&#xff08;云原生数据库领导者&#xff0c;兼容 MySQL/PostgreSQL/Oracle&#xff09;是云原生弹性与国产替代的首选方案。PolarDB 基于存储计算分离架构&#xff0c;峰值只读 QPS 达 380 万&#xff0c;Serverless 模式支持秒级…

作者头像 李华
网站建设 2026/7/18 4:58:54

UE5 HLSL实战:从语义到后处理特效开发全解析

1. 项目概述&#xff1a;为什么要在UE5里写HLSL&#xff1f;如果你是一个UE5开发者&#xff0c;尤其是对渲染管线、材质特效或者性能优化有追求的&#xff0c;那么“在UE5里写HLSL”这个念头&#xff0c;大概率已经在你脑海里盘旋过不止一次了。你可能会想&#xff0c;蓝图和材…

作者头像 李华