当前位置: 首页 > news >正文

享元模式基础设计思路

01.享元模式基础介绍

1.1 享元模式由来

面向对象技术可以很好地解决一些灵活性或可扩展性问题,但在很多情况下需要在系统中增加类和对象的个数。

当对象数量太多时,将导致运行代价过高,带来性能下降等问题。举一个简单例子,比如handler发送消息,创建大量的消息message对象,就用到了享元模式。

享元模式由来,主要是解决对象太多创建的问题:

  1. 享元模式通过共享技术实现相同或相似对象的重用。
  2. 在享元模式中通常会出现工厂模式,需要创建一个享元工厂来负责维护一个享元池(Flyweight Pool)用于存储具有相同内部状态的享元对象。
1.2 享元模式定义

享元模式(Flyweight Pattern)

运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。

享元模式的核心思想是将具有相同内部状态的对象共享,以减少内存占用。

  • 当需要创建一个对象时,首先检查是否已经存在具有相同内部状态的对象。
  • 如果存在,则重用该对象,而不是创建一个新的对象。如果不存在,则创建一个新的对象并将其添加到共享池中,以供以后使用。

在享元模式中,对象分为两种类型:内部状态(Intrinsic State)和外部状态(Extrinsic State)。

内部状态是对象共享的部分,它不会随着外部环境的变化而改变。外部状态是对象特定的部分,它会随着外部环境的变化而变化。

1.3 享元模式场景

享元模式使用场景

  1. 大量相似对象的情况: 当一个应用程序使用大量相似对象,且这些对象可以被共享时,使用享元模式可以减少内存占用。典型的例子是文本编辑器中的字符对象,其中相同字符只需创建一次并共享。
  2. 缓存: 享元模式可以用于实现缓存,特别是在需要频繁访问、计算成本高昂的数据时。将已经计算好的数据缓存起来,并在需要时重用,可以提高系统的性能。
  3. 连接池: 在数据库连接池、线程池等资源池的设计中,享元模式也经常被应用。共享可用的连接或线程对象,避免频繁地创建和销毁。

工作中遇到场

  1. 缓存,字符串常量池,线程池,连接池

作用

  1. 提高性能:生成对象成本比较高,生成之后缓存起来
  2. 降低内存消耗:能用一个对象就别创建两个对象
1.4 享元模式思考

享元模式的一些关键要点:

  • 内部状态是可以共享的,它存储在享元对象内部,并且不会随着外部环境的变化而改变。
  • 外部状态是不可共享的,它由客户端传递给享元对象,并且会随着外部环境的变化而变化。
  • 享元工厂(Flyweight Factory)负责创建和管理享元对象。它维护一个享元对象的池,根据客户端的请求返回已存在的享元对象或创建新的享元对象。
  • 客户端通过享元工厂获取享元对象,并将外部状态传递给享元对象。客户端可以共享相同的享元对象,从而减少内存占用。
1.5 核心思想是什么

享元模式的核心思想是将对象的内部状态和外部状态分离。内部状态可以被多个对象共享,而外部状态则由客户端传递给享元对象。通过共享内部状态,可以减少对象的数量,从而减少内存占用。

02.享元模式原理与实现

2.1 罗列一个场景

接单做网站:要求做产品展示网站,有的人希望是新闻发布形式的,有的人希望是博客形式的,也有还是原来的产品图片加说明形式的。

因为他们找我们来做的人的需求只是有一些小小的差别。但是不可能有100家企业来找你做网站,你难道去申请100个服务器,用100个数据库,然后用类似的代码复制100遍,去实现吗?

不使用享元模式 - 接单做网站。

public class FlyweightWebSite { public static void main(String[] args) { test1(); } public static void test1() { WebSite fx = new WebSite("产品展示"); fx.use(); WebSite fy = new WebSite("产品展示"); fy.use(); WebSite fz = new WebSite("产品展示"); fz.use(); WebSite fl = new WebSite("博客"); fl.use(); WebSite fm = new WebSite("博客"); fm.use(); WebSite fn = new WebSite("博客"); fn.use(); } //网站 public static class WebSite { private String name = ""; public WebSite(String name) { this.name = name; } public void use() { System.out.println("网站分类:" + name); } } }
2.2 用例子理解享元

如果要做三个产品展示,三个博客的网站,就需要六个网站类的实例,而其实它们本质上都是一样的代码,如果网站增多,实例也就随着增多。

利用享元模式做成第一版本的网站,代码如下所示:

public static void test2() { System.out.println("使用享元模式做的第一版网站"); WebSiteFactory webSiteFactory = new WebSiteFactory(); AbsWebSite siteCategory = webSiteFactory.getWebSiteCategory("产品展示"); siteCategory.use(); AbsWebSite siteCategory1 = webSiteFactory.getWebSiteCategory("产品展示"); siteCategory1.use(); AbsWebSite siteCategory2 = webSiteFactory.getWebSiteCategory("产品展示"); siteCategory2.use(); AbsWebSite siteCategory3 = webSiteFactory.getWebSiteCategory("博客"); siteCategory3.use(); AbsWebSite siteCategory4 = webSiteFactory.getWebSiteCategory("博客"); siteCategory4.use(); AbsWebSite siteCategory5 = webSiteFactory.getWebSiteCategory("博客"); siteCategory5.use(); System.out.println("总共创建了 " + webSiteFactory.getWebSiteCount() + " 个实例"); } /** * 网站抽象类 */ public abstract static class AbsWebSite { public abstract void use(); } /** * 具体网站类 */ public static class ConcreteWebSite extends AbsWebSite { private String name = ""; public ConcreteWebSite(String name) { this.name = name; } @Override public void use() { System.out.println("网站分类:" + name); } } /** * 网站工厂类 */ public static class WebSiteFactory { private HashMap<String, AbsWebSite> flyweights = new HashMap<>(); /** * 获得网站分类 * * @param key key * @return AbsWebSite */ public AbsWebSite getWebSiteCategory(String key) { if (!flyweights.contains(key)) { flyweights.put(key, new ConcreteWebSite(key)); } return flyweights.get(key); } /** * 获得网站实例个数 * * @return 数量 */ public int getWebSiteCount() { return flyweights.size(); } }

上面案例遇到了一些问题

基本实现了享元模式的共享对象的目的,也就是说,不管建个网站,只要是 ‘产品展示’ ,都是一样的,只要是 ‘博客’ ,也是完全相同的。

但这样是有问题的,以上这样建的网站不是一家客户的,它们的数据不会相同,所以至少它们都应该有不同的账号。这样写没有体现对象间的不同,只体现了它们共享的部分。

2.3 内部和外部状态

内部状态 - 外部状态 - 享元模式 - 接单做网站

在享元对象内部并且不会随环境改变而改变的共享部分,可以称为享元对象的内部状态,随环境改变而改变的、不可以共享的状态就是外部状态。

事实上,享元模式可以避免大量非常相似类的开销。在程序设计中,有时需要生成大量细粒度的类实例来表示数据。如果能发现这些实例除了几个 参数外基本上都是相同的,有时就能够大幅度地减少需要实例化的类的数量。如果能把那些参数移到类实例的外面,在方法调用时将它们传递进来, 就可以通过共享大幅度地减少单个实例的数目。

也就是说,享元模式 Flyweight 执行时所需的状态有内部的也可能有外部的,内部状态存储于 ConcreteFlyweight 对象之中,而外部对象则应该考虑由客户端对象存储或计 算,当调用Flyweight对象的操作时,将该状态传递给它。

客户的账号就是外部状态,应该由专门的对象来处理。

/** * 用户类,用于网站的客户账号,是"网站"类的外部状态。 */ public static class User { private String name; public User(String name) { this.name = name; } public String getName() { return name; } } /** * 网站抽象类 */ public abstract static class AbsWebSite2 { public abstract void use(User user); } public static class ConcreteWebSite2 extends AbsWebSite2 { private String name = ""; public ConcreteWebSite2(String name) { this.name = name; } @Override public void use(User user) { System.out.println("网站分类:" + name + " 来自客户:" + user.getName() + "的需求"); } }

打印数据如下所示,这样就可以协调内部与外部状态了。

使用享元模式做的第二版网站,添加外部状态 网站分类:产品展示 来自客户:打工充0的需求 网站分类:产品展示 来自客户:打工充1的需求 网站分类:产品展示 来自客户:打工充2的需求 网站分类:博客 来自客户:打工充3的需求 网站分类:博客 来自客户:打工充4的需求 网站分类:博客 来自客户:打工充5的需求 总共创建了 2 个实例
2.4 享元模式实现

享元模式实现的角色

  1. Flyweight:享元对象抽象基类或接口。
  2. ConcreteFlyweight:具体的享元对象。
  3. FlyweightFactory:享元工厂,负责管理享元对象池和创建享元对象。
  4. UnsharedConcreteFlyweight:非共享具体享元类

享元模式的基本实现代码如下所示

/** * 享元模式基本实现,比较官方的demo案例 */ public class FlyweightDemo { public static void main(String[] args) { int extrinsicState = 30; FlyweightFactory factory = new FlyweightFactory(); Flyweight flyweightA = factory.getFlyweight("A"); flyweightA.operation(--extrinsicState); Flyweight flyweightB = factory.getFlyweight("B"); flyweightB.operation(--extrinsicState); Flyweight flyweightC = factory.getFlyweight("C"); flyweightC.operation(--extrinsicState); // 不要共享的 UnsharedConcreteFlyweight unsharedFly = new UnsharedConcreteFlyweight(); unsharedFly.operation(--extrinsicState); } /** * Flyweight类是所有具体享元类的超类或接口,通过这个接口, Flyweight可以接受并作用于外部状态。 */ public static abstract class Flyweight { public abstract void operation(int extrinsicState); } /** * ConcreteFlyweight是继承Flyweight超类或实现Flyweight接口,并为内部状态增加存储空间。 * 需要共享的具体Flyweight子类 */ public static class ConcreteFlyweight extends Flyweight { @Override public void operation(int extrinsicState) { System.out.println("需要共享的具体Flyweight子类:" + extrinsicState); } } /** * 需要共享的具体Flyweight子类 * UnsharedConcreteFlyweight是指那些不需要共享的Flyweight子类。因为Flyweight接口共享成为可能,但它并不强制共享。 */ public static class UnsharedConcreteFlyweight extends Flyweight { @Override public void operation(int extrinsicState) { System.out.println("不需要共享的具体Flyweight子类:" + extrinsicState); } } /** * 享元工厂 */ public static class FlyweightFactory { private final HashMap<String, Flyweight> flyweights = new HashMap<>(); /** * 初始化工厂三个实例 */ public FlyweightFactory() { flyweights.put("A", new ConcreteFlyweight()); flyweights.put("B", new ConcreteFlyweight()); flyweights.put("C", new ConcreteFlyweight()); } /** * 根据客户端请求,获得已生成的实例 * * @param key key * @return Flyweight */ public Flyweight getFlyweight(String key) { return flyweights.get(key); } } }

03.享元模式分析

3.1 享元模式VS单例

在单例模式中,一个类只能创建一个对象,而在享元模式中,一个类可以创建多个对象,每个对象被多处代码引用共享。

实际上,享元模式有点类似于之前讲到的单例的变体:多例。

区别两种设计模式,不能光看代码实现,而是要看设计意图,也就是要解决的问题。

  1. 尽管从代码实现上来看,享元模式和多例有很多相似之处,但从设计意图上来看,它们是完全不同的。
  2. 应用享元模式是为了对象复用,节省内存,而应用多例模式是为了限制对象的个数。
3.2 享元模式VS缓存

在享元模式的实现中,我们通过工厂类来“缓存”已经创建好的对象。

这里的“缓存”实际上是“存储”的意思,跟我们平时所说的“数据库缓存”“CPU 缓存”“MemCache 缓存”是两回事。

我们平时所讲的缓存,主要是为了提高访问效率,而非复用。

3.3 享元模式VS对象池

简单解释一下对象池。

像 C++ 这样的编程语言,内存的管理是由程序员负责的。为了避免频繁地进行对象创建和释放导致内存碎片,我们可以预先申请一片连续的内存空间,也就是这里说的对象池。

每次创建对象时,我们从对象池中直接取出一个空闲对象来使用,对象使用完成之后,再放回到对象池中以供后续复用,而非直接释放掉。

对象池、连接池(比如数据库连接池)、线程池等也是为了复用,那它们跟享元模式有什么区别呢?

虽然对象池、连接池、线程池、享元模式都是为了复用,但是抠一抠“复用”这个字眼的话,对象池、连接池、线程池等池化技术中的“复用”和享元模式中的“复用”实际上是不同的概念。

池化技术中的“复用”可以理解为“重复使用”,主要目的是节省时间(比如从数据库池中取一个连接,不需要重新创建)。在任意时刻,每一个对象、连接、线程,并不会被多处使用,而是被一个使用者独占,当使用完成之后,放回到池中,再由其他使用者重复利用。

享元模式中的“复用”可以理解为“共享使用”,在整个生命周期中,都是被所有使用者共享的,主要目的是节省空间。

04.享元模式应用解析

4.1 Integer享元模式应用

先来看下面这样一段代码。你可以先思考下,这段代码会输出什么样的结果。

private static void test() { Integer i1 = 31; Integer i2 = 31; Integer i3 = 129; Integer i4 = 129; System.out.println(i1 == i2); System.out.println(i3 == i4); //true //false }

你可能会觉得,i1 和 i2 值都是 56,i3 和 i4 值都是 129,i1 跟 i2 值相等,i3 跟 i4 值相等,所以输出结果应该是两个 true。但其实这是错误的。因为Integer用到了缓存池的概念……

需要弄清楚下面两个问题:

  1. 如何判定两个 Java 对象是否相等(也就代码中的“==”操作符的含义)?
  2. 什么是自动装箱(Autoboxing)和自动拆箱(Unboxing)?

所谓的自动装箱,就是自动将基本数据类型转换为包装器类型。所谓的自动拆箱,也就是自动将包装器类型转化为基本数据类型。

Integer i = 31; //自动装箱 底层执行了:Integer i = Integer.valueOf(31); int j = i; //自动拆箱 底层执行了:int j = i.intValue();

为何 i3==i4 判定语句也会返回 false

这正是因为 Integer 用到了享元模式来复用对象,才导致了这样的运行结果。

当我们通过自动装箱,也就是调用 valueOf() 来创建 Integer 对象的时候,如果要创建的 Integer 对象的值在 -128 到 127 之间,会从 IntegerCache 类中直接返回,否则才调用 new 方法创建。

public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); }

这里的 IntegerCache 相当于生成享元对象的工厂类,只不过名字不叫 xxxFactory 而已。

4.2 String享元模式应用

再来看下,享元模式在 Java String 类中的应用。同样,我们还是先来看一段代码,你觉得这段代码输出的结果是什么呢?

private static void test2() { String s1 = "打工充"; String s2 = "打工充"; String s3 = new String("打工充"); System.out.println(s1 == s2); System.out.println(s1 == s3); //true //false }

跟 Integer 类的设计思路相似,String 类利用享元模式来复用相同的字符串常量(也就是代码中的“打工充”)。

JVM 会专门开辟一块存储区来存储字符串常量,这块存储区叫作“字符串常量池”。

4.3 线程池享元模式应用
4.4 Handler享元模式应用

Handler 消息机制中的 Message 消息池就是使用享元模式复用了 Message 对象

遇到问题说明:如果使用 new Message() 会构造大量的 Message 对象。通过new创建Message就会创建大量重复的Message对象,导致内存占用率高,频繁GC等问题。

使用享元模式:使用 Message 时一般是用 Message.obtain 来获取消息。通过享元模式创建一个大小为50的消息池,避免了上述问题的产生。

Message享元模式分析:Message相当于承担了享元模式中3个元素的职责,既是Flyweight抽象,又是ConcreteFlyweight角色,同时又承担了FlyweightFactory管理对象池的职责。

注意点:Message的享元模式并不是经典的实现方式,它没有内部,外部状态,集各个职责于一身,甚至更像一个对象池。

05.享元模式总结

5.1 有哪些优点

优点

  1. 内存优化:通过共享对象的内部状态,可以大大减少对象的数量,从而节省内存空间。
  2. 性能提升:减少对象数量和内存占用可以提高系统的性能,特别是在处理大量细粒度对象时。
  3. 可扩展性:享元模式可以轻松扩展以支持新的外部状态,而无需修改现有的共享对象。
  4. 对象复用:通过共享对象,可以实现对象的复用,避免重复创建相同的对象,提高系统的效率和资源利用率。
5.2 有哪些弊端

缺点:

  1. 共享对象可能引入线程安全问题:如果多个线程同时访问和修改共享对象的状态,需要考虑线程安全性,以避免并发访问引发的问题。
  2. 对象状态的外部化:为了实现对象共享,部分对象状态需要外部化,这可能会增加代码复杂性和维护成本。
5.3 休闲棋类应用

享元模式更多的时候是一种底层的设计模式,但现实中也是有应用的:

比如休闲棋类。

如果不用享元模式会怎么样?

像象棋,一盘棋理论上有32棋子,那如果用常规的面向对象方式编程,每盘棋都可能有至少32个棋子对象产生。

一个游戏厅中有成千上万个“房间”,每个房间对应一个棋局。棋局要保存每个棋子的数据,比如:棋子类型(将、相、士、炮等)、棋子颜色(红方、黑方)、棋子在棋局中的位置。

思考一下下面的问题

一台服务器就很难支持更多的玩家玩围棋游戏了,比如1万棋局玩家,那就会有大量的对象,毕竟内存空间还是有限的。

那么我们如何去避免大量细粒度的对象,同时又不影响客户程序,是一个值得去思考的问题。有没有什么办法来节省内存呢?

用了享元模式主要是节省资源和降低损耗

比如说休闲游戏开发中,像象棋,它们都有大量的棋子对象,分析一下,它们的内部状态和外部状态各是什么?

象棋只有红黑两色、跳棋颜色略多一些,但也是不太变化的,所以颜色应该是棋子的内部状态,而各个棋子之间的差别主要就是位置的不同,所以方位坐标应该是棋子的外部状态。

在使用享元模式之前,记录1万个棋局,我们要创建 32 万(32*1 万)个棋子的对象。利用享元模式,我们只需要创建32个享元对象供所有棋局共享使用即可,大大节省了内存。

5.4 文本编辑器应用

使用享元模式的一个常见例子是文本编辑器中的字符对象。

在一个文本文件中,可能有大量的字符对象,它们的外部状态(例如位置、字体、颜色等)可能不同,但内部状态(例如字符代码、字符宽度等)是相同的。

通过共享具有相同字符代码的字符对象,可以大大减少内存使用。

5.5 应用环境说明

享元模式适用于大量细粒度对象的场景show(WEIBO.COM/ttarticle/p/show?id=2309405283399932969126),通过共享对象的内部状态来减少对象数量和内存占用,从而提高系统性能和资源利用率。它在需要重复创建相似对象的情况下特别有用,并且适用于多线程环境,但需要注意线程安全性。

http://www.cnnetsun.cn/news/1659263.html

相关文章:

  • PCB布局布线核心技术解析与设计实践
  • 手把手教你用Matlab/Simulink实现PMSM FOC控制(附SVPWM算法代码)
  • ASMR音频资源管理工具:高效构建个人音频库
  • UNIX设计哲学:一切皆文件的原理与应用
  • OpenClaw爆火!Token是什么?一文搞懂这个AI核心概念!
  • 高薪职业:AI大模型架构师,你需要知道的一切!
  • 告别网络限制!哔咔漫画离线下载神器使用全攻略
  • 4个突破步骤:Switch手柄PC全功能适配实现跨平台游戏自由
  • 插件冲突频发?三招让你的WPS回归清爽
  • 基于双边相关变换与TDOA技术的Matlab角度估计方法:TCT-DOA的原理与实现
  • GHelper终极指南:华硕笔记本性能优化完全手册,告别臃肿控制中心
  • Token 是万亿美元产业基石?揭秘马嘉祺被 AI 弄错背后的语言不平等!
  • 基于Uniapp + SpringBoot + Vue的商场停车场管理系统(两端:移动端、PC端)
  • Azure IoT Hub C SDK嵌入式实践:MCU低资源适配与MQTT通信
  • 手把手教你用STM32F103C8T6自制USB-Blaster,顺便做个最小系统板(附开源固件地址)
  • 2026最权威的AI写作神器实测分析
  • 从C++20 stackless协程到C++27 full-resumable协程,全链路迁移实操,含MSVC/GCC/Clang 15+16+17三端可运行代码库
  • C语言跨平台文件操作陷阱与解决方案
  • 3大突破!用开源工具ExplorerBlurMica焕新Windows文件管理器界面
  • 嵌入式C预处理器元编程:零开销可变参数宏遍历方案
  • 电网电压不平衡下三相三电平PWM整流器仿真模型探索
  • MCP23009 I²C GPIO扩展芯片驱动设计与实战
  • STM32 ISP下载机制与BootLoader深度解析
  • 对于对话中的多轮问答,OpenClaw 的答案溯源机制?
  • 输出电压采用模型预测控制(MPC)的三相逆变器 针对一步预测控制算法的不足,提出采用两步预测控...
  • IDEA 里装个 AI 助手:Amazon Q Developer for JetBrains 实测体验
  • 改进遗传算法求解分布式柔性作业车间调度问题 Matlab代码 考虑多工厂约束,以最小化最大完工...
  • 踩下油门的那一刻,P2并联混动系统开始了一场精密的能量博弈。咱们今天不聊枯燥的理论,直接钻进Simulink模型里看看这套系统怎么玩转发动机和电机的“二人转
  • Windows右键菜单管理终极指南:让您的系统更高效更整洁
  • 解释 Linux 系统中的文件系统层次结构,并举例说明重要目录的用途。