java)
文章目录前言一、核心定义二、标准体系结构图三、场景推演四、实战案例4.1 需求分析4.2 架构图4.2.1 普通方式架构图4.2.2 享元模式架构图4.3 时序图4.3.1 普通代码时序图4.3.2 享元模式时序图4.4 代码分析4.4.1 普通代码4.4.2 享元模式代码总结前言在实际开发中我们经常会遇到一种情况系统里有大量对象但这些对象中很多数据是重复的。比如游戏地图中有 10 万棵树 每棵树都有坐标、颜色、纹理、树种。 秒杀系统中有很多活动查询 每次查询都有活动名称、活动描述、开始时间、结束时间、库存信息。随着业务规模的扩张这些海量的对象会像无底洞一样吞噬系统的内存导致频繁的垃圾回收GC甚至引发系统崩溃OOM。对于这种情况如何用最少的资源办最多的事如果每次都创建一个完整的对象就像每个学生都要一本完全一样的教材但学校却给每个人重新打印一遍这显然会浪费大量资源。共享元模式就是为了解决这种“重复对象过多内存浪费严重”的问题。其核心思想是相同的内容共享不同的外部内容形成。本文代码https://github.com/likerhood/CodeDesignWork/tree/main/codedesign10.0-0 和 10.0-1一、核心定义享元模式的核心思想非常纯粹运用共享技术有效地支持大量细粒度对象的复用。它的定义可以这样理解通过共享已经存在的对象减少大量相似对象的创建从而降低内存消耗提高系统访问效率。更直白地说不要每次都 new 一个完整对象能共享的部分就共享不能共享的部分再单独传入这里的思想和原型模式比较接近。在共享元模式中最重要的是区分两个状态状态意义是否共享举例说明内在状态对象中稳定、不变、可复用的数据可以分享活动名称、活动描述、树的纹理、树的颜色外部在状态每个对象突出、经常变化的数据不分享库存数量、树的坐标、当前用户状态类比在购物节的秒似杀活动中活动ID、活动名称、活动描述、开始时间、结束时间这些通常是不变的可以共享。库存数量、已售数量、用户下单状态这些是变化的不适合共享。所以享元模式的本质是完整对象 共享对象 外部状态二、标准体系结构图享元模式的标准体系通常包含以下核心角色FlyweightFactory享元工厂负责创建和管理享元对象。当客户端请求时工厂会检查池中是否已有符合要求的对象如果有则直接返回没有则创建新对象并放入池中。Flyweight抽象享元接口规定了具体享元类必须实现的方法通常会接收外部状态作为参数。ConcreteFlyweight具体享元类实现了接口并为内部状态提供存储空间。这里的重点在于客户端不直接创建共享对象客户端通过享元工厂获取共享对象享元工厂内部维护一个对象池如果对象已经存在直接复用如果对象不存在再创建并放入对象池。获取享元对象管理对象池Clientoperation()FlyweightFactory-MapString, Flyweight poolgetFlyweight(key) : Flyweight«interface»Flyweightoperation(extrinsicState)ConcreteFlyweight-intrinsicStateoperation(extrinsicState)三、场景推演在游戏开发中通常需要渲染成千上万的树木来构成一片广袤的森林。如果每一棵树都包含完整的品种信息、高分辨率的树皮贴图模型几十MB大小以及具体的坐标位置一百万棵树就会直接撑爆显存。但仔细推演我们会发现森林里的树虽然多但“品种”其实只有寥寥几种比如橡树、松树、白桦树。树的品种和贴图模型是永远不变的内部状态而树在地图上的 X、Y 坐标是每棵树独有的外部状态。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 MapString, 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 ListTree 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()); } }最核心的地方是这句TreeTypetreeTypeTreeFactory.getTreeType(name,barkTexture,modelData);它表示种树时不要每次都创建完整树模型而是先去享元池里找有没有相同类型的树。如果有就直接复用。这些数据不再每棵树都复制一份而是相同类型的树共享一份。Tree 对象还是创建了 6 个但 TreeType 这种重量级、可共享的对象只创建了 3 个。四、实战案例4.1 需求分析秒杀活动查询接口通常会返回活动 ID、活动名称、活动描述、开始时间、结束时间、库存等信息。普通实现中每次请求都会重新创建完整的活动对象活动基础信息和库存信息一起被反复构建。但在真实秒杀场景中活动名称、描述、开始/结束时间等属于相对稳定的内部状态而库存已用数量属于频繁变化的外部状态。享元模式的核心思路就是复用稳定对象只把变化数据放在外部动态补充从而减少对象创建和内存占用。4.2 架构图4.2.1 普通方式架构图4.2.2 享元模式架构图4.3 时序图4.3.1 普通代码时序图StockActivityActivityControllerApiTestStockActivityActivityControllerApiTestqueryActivityInfo(10001L)new Activity()setId/setName/setDesc/setTimenew Stock(1000, 1)setStock(stock)return Activity4.3.2 享元模式时序图StockRedisUtilsShared ActivityActivityFactoryActivityControllerApiTestStockRedisUtilsShared ActivityActivityFactoryActivityControllerApiTestalt[缓存不存在][缓存存在]queryActivityInfo(10001L)getActivity(10001L)new Activity()activityMap.put(id, activity)return cached ActivitygetStockUsed()usednew Stock(1000, used)setStock(stock)return Activity4.4 代码分析4.4.1 普通代码普通实现的核心问题在于每次请求都会重新创建 Activity并把活动基础信息、活动时间、库存信息全部硬编码到接口逻辑里。publicActivityqueryActivityInfo(Longid){ActivityactivitynewActivity();activity.setId(10001L);activity.setName(图书嗨乐);activity.setDesc(图书优惠券分享激励分享活动第二期);activity.setStartTime(newDate());activity.setStopTime(newDate());activity.setStock(newStock(1000,1));returnactivity;}这种方式简单直接但当请求量变大时大量重复对象会被创建。活动基础信息本身并不会频繁变化却在每一次查询中重新构造造成不必要的内存和对象创建成本。4.4.2 享元模式代码享元模式版本引入 ActivityFactory通过 MapLong, Activity 缓存活动对象。活动 ID、名称、描述、时间等稳定信息作为内部状态被共享库存已用数量作为外部状态从 RedisUtils 中动态读取后重新设置。publicclassActivityFactory{staticMapLong,ActivityactivityMapnewHashMap();publicstaticActivitygetActivity(Longid){ActivityactivityactivityMap.get(id);if(nullactivity){activitynewActivity();activity.setId(10001L);activity.setName(图书嗨乐);activity.setDesc(图书优惠券分享激励分享活动第二期);activity.setStartTime(newDate());activity.setStopTime(newDate());activityMap.put(id,activity);}returnactivity;}}publicActivityqueryActivityInfo(Longid){ActivityactivityActivityFactory.getActivity(id);StockstocknewStock(1000,redisUtils.getStockUsed());activity.setStock(stock);returnactivity;}整体来看享元模式把“不变的活动信息”和“变化的库存信息”拆开处理前者缓存复用后者按请求动态补充。这样可以减少重复对象创建也让业务结构更贴合高并发秒杀查询场景。总结享元模式是空间换时间或共享换空间的经典体现。通过构建缓存池和状态拆分它能在高并发、大数据量的场景下将内存占用成千上万倍地压缩。虽然它极大提升了性能但也引入了状态分离的系统复杂度。在实际应用中如 Java 的String常量池、数据库连接池、线程池我们需要时刻警惕多线程环境下的线程安全问题确保享元对象的“内部状态”绝对不可被篡改。