享元模式基础设计思路

张开发
2026/5/21 20:31:07 15 分钟阅读
享元模式基础设计思路
01.享元模式基础介绍1.1 享元模式由来面向对象技术可以很好地解决一些灵活性或可扩展性问题但在很多情况下需要在系统中增加类和对象的个数。当对象数量太多时将导致运行代价过高带来性能下降等问题。举一个简单例子比如handler发送消息创建大量的消息message对象就用到了享元模式。享元模式由来主要是解决对象太多创建的问题享元模式通过共享技术实现相同或相似对象的重用。在享元模式中通常会出现工厂模式需要创建一个享元工厂来负责维护一个享元池(Flyweight Pool)用于存储具有相同内部状态的享元对象。1.2 享元模式定义享元模式(Flyweight Pattern)运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象而这些对象都很相似状态变化很小可以实现对象的多次复用。享元模式的核心思想是将具有相同内部状态的对象共享以减少内存占用。当需要创建一个对象时首先检查是否已经存在具有相同内部状态的对象。如果存在则重用该对象而不是创建一个新的对象。如果不存在则创建一个新的对象并将其添加到共享池中以供以后使用。在享元模式中对象分为两种类型内部状态Intrinsic State和外部状态Extrinsic State。内部状态是对象共享的部分它不会随着外部环境的变化而改变。外部状态是对象特定的部分它会随着外部环境的变化而变化。1.3 享元模式场景享元模式使用场景大量相似对象的情况 当一个应用程序使用大量相似对象且这些对象可以被共享时使用享元模式可以减少内存占用。典型的例子是文本编辑器中的字符对象其中相同字符只需创建一次并共享。缓存 享元模式可以用于实现缓存特别是在需要频繁访问、计算成本高昂的数据时。将已经计算好的数据缓存起来并在需要时重用可以提高系统的性能。连接池 在数据库连接池、线程池等资源池的设计中享元模式也经常被应用。共享可用的连接或线程对象避免频繁地创建和销毁。工作中遇到场缓存字符串常量池线程池连接池作用提高性能:生成对象成本比较高,生成之后缓存起来降低内存消耗:能用一个对象就别创建两个对象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 HashMapString, 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 享元模式实现享元模式实现的角色Flyweight享元对象抽象基类或接口。ConcreteFlyweight具体的享元对象。FlyweightFactory享元工厂负责管理享元对象池和创建享元对象。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 HashMapString, 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单例在单例模式中一个类只能创建一个对象而在享元模式中一个类可以创建多个对象每个对象被多处代码引用共享。实际上享元模式有点类似于之前讲到的单例的变体多例。区别两种设计模式不能光看代码实现而是要看设计意图也就是要解决的问题。尽管从代码实现上来看享元模式和多例有很多相似之处但从设计意图上来看它们是完全不同的。应用享元模式是为了对象复用节省内存而应用多例模式是为了限制对象的个数。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 值都是 56i3 和 i4 值都是 129i1 跟 i2 值相等i3 跟 i4 值相等所以输出结果应该是两个 true。但其实这是错误的。因为Integer用到了缓存池的概念……需要弄清楚下面两个问题如何判定两个 Java 对象是否相等也就代码中的“”操作符的含义什么是自动装箱Autoboxing和自动拆箱Unboxing所谓的自动装箱就是自动将基本数据类型转换为包装器类型。所谓的自动拆箱也就是自动将包装器类型转化为基本数据类型。Integer i 31; //自动装箱 底层执行了Integer i Integer.valueOf(31); int j i; //自动拆箱 底层执行了int j i.intValue();为何 i3i4 判定语句也会返回 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 有哪些优点优点内存优化通过共享对象的内部状态可以大大减少对象的数量从而节省内存空间。性能提升减少对象数量和内存占用可以提高系统的性能特别是在处理大量细粒度对象时。可扩展性享元模式可以轻松扩展以支持新的外部状态而无需修改现有的共享对象。对象复用通过共享对象可以实现对象的复用避免重复创建相同的对象提高系统的效率和资源利用率。5.2 有哪些弊端缺点共享对象可能引入线程安全问题如果多个线程同时访问和修改共享对象的状态需要考虑线程安全性以避免并发访问引发的问题。对象状态的外部化为了实现对象共享部分对象状态需要外部化这可能会增加代码复杂性和维护成本。5.3 休闲棋类应用享元模式更多的时候是一种底层的设计模式但现实中也是有应用的比如休闲棋类。如果不用享元模式会怎么样像象棋一盘棋理论上有32棋子那如果用常规的面向对象方式编程每盘棋都可能有至少32个棋子对象产生。一个游戏厅中有成千上万个“房间”每个房间对应一个棋局。棋局要保存每个棋子的数据比如棋子类型将、相、士、炮等、棋子颜色红方、黑方、棋子在棋局中的位置。思考一下下面的问题一台服务器就很难支持更多的玩家玩围棋游戏了比如1万棋局玩家那就会有大量的对象毕竟内存空间还是有限的。那么我们如何去避免大量细粒度的对象同时又不影响客户程序是一个值得去思考的问题。有没有什么办法来节省内存呢用了享元模式主要是节省资源和降低损耗比如说休闲游戏开发中像象棋它们都有大量的棋子对象分析一下它们的内部状态和外部状态各是什么象棋只有红黑两色、跳棋颜色略多一些但也是不太变化的所以颜色应该是棋子的内部状态而各个棋子之间的差别主要就是位置的不同所以方位坐标应该是棋子的外部状态。在使用享元模式之前记录1万个棋局我们要创建 32 万32*1 万个棋子的对象。利用享元模式我们只需要创建32个享元对象供所有棋局共享使用即可大大节省了内存。5.4 文本编辑器应用使用享元模式的一个常见例子是文本编辑器中的字符对象。在一个文本文件中可能有大量的字符对象它们的外部状态例如位置、字体、颜色等可能不同但内部状态例如字符代码、字符宽度等是相同的。通过共享具有相同字符代码的字符对象可以大大减少内存使用。5.5 应用环境说明享元模式适用于大量细粒度对象的场景showWEIBO.COM/ttarticle/p/show?id2309405283399932969126通过共享对象的内部状态来减少对象数量和内存占用从而提高系统性能和资源利用率。它在需要重复创建相似对象的情况下特别有用并且适用于多线程环境但需要注意线程安全性。

更多文章