news 2026/10/18 1:56:32

11-A-享元模式详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
11-A-享元模式详解

11-A-享元模式详解

关键词:享元、Flyweight、对象池、内部状态、外部状态、Integer 缓存、规格字典

享元模式解决一个问题:大量相似对象占内存太多,把"可共享的部分"抽出来复用。一亿个商品 SKU 都要引用"颜色:曜石黑"这个规格对象,没必要一亿份——共享一份,把"每个 SKU 不同的部分"留在外面传参。


📖 术语速查表

术语人话解释
享元(Flyweight)被共享的细粒度对象
内部状态可以共享、不随场景变化的状态(如规格名"曜石黑")——存在享元里
外部状态随场景变化、不能共享的状态(如某个 SKU 的价格)——调用时传参
享元工厂/池管理共享实例的工厂,通常ConcurrentHashMap.computeIfAbsent
对象池享元的工程化近亲:复用昂贵对象(连接、线程)

一、不用享元的问题

电商商品的规格体系:每个 SKU 由多个"规格项"组成(颜色、尺寸、版本)。系统里有 500 万 SKU:

// ❌ 每个 SKU 都 new 一套规格对象publicclassSku{privateSpecItemcolor;// new SpecItem("颜色", "曜石黑", "#000000", ...)privateSpecItemsize;// new SpecItem("尺寸", "256G", ...)privateSpecItemversion;// new SpecItem("版本", "国行", ...)privateBigDecimalprice;}

全站其实只有 200 种颜色、50 种尺寸、10 种版本——但内存里躺着 1500 万个SpecItem对象。每个对象 64 字节的话就是近 1GB,而实际只需要 260 个。


二、模式结构:内部状态进享元,外部状态传参

computeIfAbsent 复用

computeIfAbsent 复用

引用

引用

引用

SpecItemFactory 享元池
ConcurrentHashMap

SpecItem 曜石黑
共享一份

SpecItem 256G
共享一份

SKU-1
price = 5999

SKU-2
price = 6299

拆分原则:

  • 内部状态(规格名、色值、排序权重):所有 SKU 一样 → 存进享元,共享;
  • 外部状态(这个 SKU 的价格、库存):每个 SKU 不同 → 留在 SKU 里或调用时传参。

三、业务场景:商品规格字典共享

享元对象(不可变是共享的前提)

/** 规格项享元:全字段 final——不可变对象才能安全共享 */publicfinalclassSpecItem{privatefinalStringspecName;// 内部状态:规格名,如"颜色"privatefinalStringspecValue;// 内部状态:规格值,如"曜石黑"privatefinalStringcolorCode;// 内部状态:色值privatefinalintsortOrder;// 内部状态:展示排序SpecItem(StringspecName,StringspecValue,StringcolorCode,intsortOrder){this.specName=specName;this.specValue=specValue;this.colorCode=colorCode;this.sortOrder=sortOrder;}/** 外部状态通过参数传入,不存在享元里 */publicStringrenderDisplay(BigDecimalskuPrice){returnspecName+": "+specValue+"(¥"+skuPrice+")";}// 只有 getter,没有 setterpublicStringgetSpecValue(){returnspecValue;}}

享元工厂

@ComponentpublicclassSpecItemFactory{/** 享元池:key = 规格名 + 规格值 */privatefinalConcurrentHashMap<String,SpecItem>pool=newConcurrentHashMap<>();publicSpecItemget(StringspecName,StringspecValue){Stringkey=specName+":"+specValue;// computeIfAbsent 保证并发下同一 key 只创建一次returnpool.computeIfAbsent(key,k->loadFromDb(specName,specValue));}privateSpecItemloadFromDb(StringspecName,StringspecValue){// 从规格字典表加载(颜色/色值/排序)SpecDictdict=specDictDao.find(specName,specValue);returnnewSpecItem(specName,specValue,dict.getColorCode(),dict.getSortOrder());}/** 池大小监控:暴露给 Prometheus,防止 key 设计错误导致池无限膨胀 */publicintpoolSize(){returnpool.size();}}

使用方

publicclassSkuAssembler{privatefinalSpecItemFactoryspecItemFactory;publicSkuassemble(SkuRawDataraw){Skusku=newSku();// 500 万个 SKU 共享 260 个 SpecItem 实例sku.setColor(specItemFactory.get("颜色",raw.getColor()));sku.setSize(specItemFactory.get("尺寸",raw.getSize()));sku.setPrice(raw.getPrice());// 外部状态留在 SKU 自己身上returnsku;}}

效果:1500 万个SpecItem→ 260 个,内存从近 1GB 降到 KB 级。


四、JDK 里最熟悉的享元:Integer 缓存

Integera=127,b=127;System.out.println(a==b);// true —— 共享了缓存池里的同一个对象Integerc=128,d=128;System.out.println(c==d);// false —— 超出缓存范围,各自 new

Integer.valueOf()内部就是享元工厂:IntegerCache预创建 -128~127(上界可用-XX:AutoBoxCacheHigh调)。这也是"锁Long skuId可能锁错对象"的根源(见 02-01-B-JMM与volatile-synchronized面试与生产事故实战)——两个 127 是同一个对象,两个 128 不是。

同类实现:Long/Short/Byte/Character缓存、String常量池、Boolean.valueOf()。


五、享元 vs 对象池 vs 单例

概念共享什么生命周期典型例子
享元不可变的细粒度对象,用完不还长期驻留池里Integer 缓存、规格字典
对象池昂贵的可变资源,借了要还借出→使用→归还复用数据库连接池、线程池
单例全局唯一的服务对象全局一份配置客户端

关键区别:享元的对象是"大家同时用"(因为不可变所以安全),连接池的连接是"同一时刻只能一个人用"(可变所以要独占借还)。


六、框架中的同款实现

框架应用说明
JDKInteger.valueOf()、String 常量池最经典的享元
NettyPooledByteBufAllocator内存块复用,享元+对象池的混合体
数据库连接池HikariCP对象池形态(借还模型)
游戏引擎子弹/粒子对象池高频创建销毁的对象复用
Caffeine/Guava Cache本地缓存本身“缓存即享元池”——很多享元场景直接用 Caffeine 实现(带容量上限和过期)

七、常见误用与生产事故

误用一:共享了可变对象

// ❌ SpecItem 有 setter,还被共享——A 业务改了 colorCode,B 业务看到的也变了publicclassSpecItem{privateStringcolorCode;publicvoidsetColorCode(Stringc){this.colorCode=c;}}

享元的铁律:必须不可变(全 final + 无 setter + 集合字段防御性拷贝)。做不到不可变,就不要共享。

误用二:池的 key 里混入了外部状态

// ❌ key 带了价格——每个价格一个实例,池退化成"每个 SKU 一份",享元失效pool.computeIfAbsent(specName+":"+specValue+":"+price,...);

key 只能由内部状态组成。判断方法:如果 key 的某个组成部分"每个调用方都不一样",它就是外部状态,不该进 key。

事故:享元池无上限导致内存泄漏

某系统用享元缓存"用户收货地址解析结果",key 是完整地址字符串——地址是无限基数(每个用户都不一样),池只增不减,运行两周后 Old 区被 2000 万个"享元"占满,频繁 Full GC 后 OOM。

修复:

  1. 享元只适合有限基数的字典类数据(规格/地区码/配置模板),无限基数的数据不配进享元池;
  2. 必须带上限:用 Caffeine 替代裸ConcurrentHashMap(maximumSize(10_000)+expireAfterAccess);
  3. 池大小打点监控,超过预期告警——任何"只进不出"的 Map 都是内存泄漏嫌疑人(同款问题见 01-06-A-JVM排查实战详解)。

八、总结

  1. 享元的核心:内部状态(可共享)存享元,外部状态(因人而异)传参数——用共享换内存。
  2. 两个前提:对象数量巨大且大部分状态可共享;享元必须不可变。
  3. 实现三件套:不可变享元类 +computeIfAbsent享元工厂 + 池大小监控。
  4. 适用边界:有限基数的字典类数据(规格/地区/模板);无限基数数据进池 = 内存泄漏,宁可用带淘汰的 Caffeine。
  5. 辨析:享元是"大家同时用不可变对象",对象池是"排队借还可变资源",别混为一谈。

📌上一篇:10-A-外观模式详解

📌下一篇:12-A-代理模式详解

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

MTCNN源码拆解:P-Net/R-Net/O-Net级联与关键点定位实战

简介&#xff1a;MTCNN&#xff08;多任务级联卷积神经网络&#xff09;是人脸检测中的经典算法&#xff0c;由P-Net、R-Net、O-Net三级网络逐级精化完成候选框筛选与关键点定位。这份代码实现覆盖网络结构定义、候选框生成、关键点回归、训练与测试等完整环节&#xff0c;适合…

作者头像 李华
网站建设 2026/10/18 1:55:10

AI重写嵌入式工具链:从链接脚本到调试日志的实战路径

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/18 1:54:36

YOLOv8s OpenVINO IR模型:开箱即用部署与推理避坑指南

简介&#xff1a;本资源为YOLOv8s-Detect模型转换后的OpenVINO推理模型包&#xff0c;面向需要在边缘计算、嵌入式设备上部署实时目标检测的开发者与算法工程师&#xff0c;可解决模型跨平台高效推理与硬件加速的问题。压缩包共3个文件&#xff0c;包含xml与bin两类OpenVINO IR…

作者头像 李华
网站建设 2026/10/18 1:53:42

Folia 歌词动画播放器安装指南:从下载到启动的分系统避坑讲解

Folia 歌词动画播放器安装指南&#xff1a;从下载到启动的分系统避坑讲解 【免费下载链接】folia-major 专注于绚丽的歌词动画效果的本地音乐/navidrome/第三方多平台在线音乐播放器 项目地址: https://gitcode.com/GitHub_Trending/fo/folia-major 在国内下载 Folia&am…

作者头像 李华