享元设计模式(Flyweight Pattern)详解

一、模式概述

1.1 简介

享元模式(Flyweight Pattern)是一种结构型设计模式,它通过共享技术来高效地支持大量细粒度对象的复用。其核心思想是:将对象的"内部状态"(可共享、不随环境变化)与"外部状态"(不可共享、随环境变化)分离,将可共享的部分提取为共享对象,从而大幅减少内存占用,提升系统性能。

"享元"是 Flyweight 的音义兼顾译名。"享"取"分享、共享"之义,"元"取"元素、要旨"之义;音节上"享(xiǎng) 元(yuán)"与 Flyweight 只是节奏上的大致呼应,并非严格音译。这一译名的核心是"分享元素"——即以共享的方式复用重复元素,从而精简内存占用。

1.2 模式别名

  • 蝇量级模式(少见别名,部分早期中文资料使用)
  • 轻量级模式(强调"轻"——对象轻量)
  • 缓存模式(Cache Pattern,部分视角下可视作对象级缓存的特例)

1.3 所属类别

GoF(Gang of Four)23 种设计模式之一,属于结构型模式(Structural Pattern)。


二、定义与意图

2.1 官方定义

意图:运用共享技术有效地支持大量细粒度的对象。

Use sharing to support large numbers of fine-grained objects efficiently.

——《Design Patterns: Elements of Reusable Object-Oriented Software》

2.2 设计目标

目标 说明
减少内存占用 通过共享相同状态的内部数据,避免重复创建对象
降低对象数量 将海量对象数量压缩到可控范围
提升性能 减少 GC 压力,提高系统吞吐量
抽象共享机制 为具有相同特征的对象提供统一的复用接口

三、核心概念

3.1 内部状态 vs 外部状态

这是理解享元模式最关键的概念:

特性 内部状态(Intrinsic State) 外部状态(Extrinsic State)
是否可共享 ✅ 可共享 ❌ 不可共享
是否随环境变化 ❌ 不变 ✅ 变化
存储位置 存储在享元对象内部 由客户端传入或由外部容器维护
典型示例 字符的字体、大小、颜色 字符在文档中的位置
生命周期 与享元对象共存亡 随业务上下文变化

3.2 关键角色

享元模式通常包含以下角色:

  • Flyweight(抽象享元角色):定义对象的公共接口,接收并作用于外部状态
  • ConcreteFlyweight(具体享元角色):实现抽象享元接口,存储内部状态
  • UnsharedConcreteFlyweight(不可共享享元角色):不需要共享的子类实现
  • FlyweightFactory(享元工厂角色):创建并管理享元对象,负责池化缓存
  • Client(客户端):维护外部状态,通过工厂获取享元并组合使用

💡 5 类角色用人话说:

  • 抽象享元:一个接口,规定"享元能干什么"(比如接受位置/颜色这种随场景变的参数)。
  • 具体享元:接口的实现者,保存不随场景变的那部分信息(字体、颜色、形状)。
  • 不可共享享元:也是接口的实现者,但不进入工厂池——典型的用法是做"复合容器"(一个棋盘、一棵树),本身不需要共享,里头装着的子节点才共享。
  • 享元工厂:唯一允许创建享元的地方,同一份内部状态只造一次,下次直接返。
  • 客户端:拿着外部状态(位置、坐标等每次都不同的参数)去找工厂要享元,拿到后用。
    简单场景下只会用到前 4 类(第 5 类"不可共享享元"留给复合结构出现时登场,见"复合享元"示例)。

📌 关于 UnsharedConcreteFlyweight(不可共享享元):这是 GoF 原著列出的第 3 种角色,但单一池化场景(如纯字符池、纯棋子池)确实不需要它。本档的"复合享元示例"是唯一完整呈现 5 个角色的代码——在那里 CompositeFlyweightImpl 即扮演该角色。


四、模式结构

4.1 UML 类图

classDiagram direction LR class Flyweight { <<interface>> +operation(extrinsicState) } class ConcreteFlyweight { -intrinsicState +operation(extrinsicState) } class UnsharedConcreteFlyweight { -allState +operation(extrinsicState) } class FlyweightFactory { -flyweights: HashMap +getFlyweight(key) Flyweight +getFlyweightCount() int } class Client { -flyweightFactory +doSomething() } Flyweight <|.. ConcreteFlyweight Flyweight <|.. UnsharedConcreteFlyweight FlyweightFactory --> Flyweight : 创建管理 Client --> FlyweightFactory : 获取享元 Client --> Flyweight : 调用操作

4.2 协作时序图

sequenceDiagram participant C as 客户端 participant F as 享元工厂 participant P as 享元池 participant FW as 具体享元 C->>F: getFlyweight(key) alt 池中已存在 F->>P: 查询 key P-->>F: 返回已有享元 else 池中不存在 F->>FW: new ConcreteFlyweight() FW-->>F: 创建新享元 F->>P: 存入池中 end F-->>C: 返回享元对象 C->>FW: operation(externalState) FW-->>C: 执行结果

4.3 对象数量对比

以英文字母场景为例,假设一段文本包含 10000 个字符。如果不使用享元模式,每出现一次字符就创建一个新对象;如果使用享元模式,相同字符将共享同一个对象。

flowchart LR A1["字符 H"]:::old -->|"享元工厂"| B1["共享对象 H"]:::new A2["字符 e"]:::old -->|"享元工厂"| B2["共享对象 e"]:::new A3["字符 l 重复一"]:::old -->|"享元工厂"| B3["共享对象 l"]:::new A4["字符 l 重复二"]:::old -->|"享元工厂"| B3 A5["字符 o"]:::old -->|"享元工厂"| B4["共享对象 o"]:::new A6["省略其余 9995 个字符"]:::old -->|"享元工厂"| B5["省略其余约 48 种共享对象"]:::new classDef old fill:#FF6B6B,stroke:#C71585,color:#fff classDef new fill:#51CF66,stroke:#228B22,color:#fff

对比说明:左侧红色节点是享元前每个字符对应的独立对象(如 "Hello" 中两个 l 是两个对象);右侧绿色节点是享元后的共享对象(两个 l 共用一个享元 B3)。10000 个字符最终仅需约 52 种共享对象(区分大小写),内存占用压缩近 200 倍。

4.4 对象数量对比表

方案 对象数量级 内存占用 适用规模
不使用享元 10000 个独立对象 极高 不推荐
使用享元 约 26 个(不区分大小写)至 52 个(区分大小写)共享对象 极低 推荐

五、适用场景

5.1 典型应用场景

flowchart TD A[判断是否需要享元模式] --> B{1. 系统中存在大量相似对象?} B -->|否| C[❌ 不适用享元模式] B -->|是| D{2. 对象的大部分状态可外部化?} D -->|否| E[❌ 不适用享元模式] D -->|是| F{3. 对象数量庞大导致内存压力?} F -->|否| G[🟡 技术可行,但收益不显著] F -->|是| H{4. 共享是否线程安全?} H -->|否| I[⚠️ 适用,但需补充同步机制] H -->|是| J[✅ 适用享元模式] style J fill:#90EE90 style C fill:#FFB6C1 style E fill:#FFB6C1 style G fill:#FFE4B5 style I fill:#FFD580

5.1.1 决策流程的四大判据

该流程通过四层递进筛选判断是否需要使用享元模式,每一层都在回答一个独立问题,必须按顺序排查:

flowchart LR A[第一道筛子<br/>业务必要性] --> B[第二道筛子<br/>技术可行性] --> C[第三道筛子<br/>经济性] --> D[第四道筛子<br/>实施细节] A -.->|量级特征| E[是否存在大量相似对象] B -.->|结构特征| F[状态能否外部化] C -.->|资源代价| G[是否真有内存压力] D -.->|编码条件| H[线程安全能否保证] style A fill:#FFE4B5 style B fill:#87CEEB style C fill:#98FB98 style D fill:#DDA0DD

5.1.2 为什么是这个顺序——四道筛子的递进逻辑

第一道筛子:业务必要性 —— 有没有「大量相似对象」?

这是享元模式的前提条件,决定"用不用得上"。

  • 问的是业务的量级特征:是否存在大量对象?这些对象之间是否高度相似?
  • 如果对象数量本身就少(例如 10 个以内),或者对象之间没有相似性,享元池里只能存 1 至 N 个对象,共享机制形同虚设,反而徒增工厂、缓存、键管理的开销。
  • 典型反例:每台服务器只有 3 个配置项的配置文件,不需要享元。
  • 典型正例:10000 个字符(只有约 26 到 52 种可能值)→ 大量相似对象,享元价值巨大。

为什么先问这一条? 因为它是最基础的"准入资格"。没有"大量相似"这个土壤,后面所有讨论都没有意义。这是一条业务侧的必要条件。

第二道筛子:技术可行性 —— 状态能不能「外部化」(能否把 bulk 剥离成 extrinsic)?

这是享元模式的技术核心,决定"用得了用不了"。

  • 问的是对象的结构特征:是否能清晰地把状态拆分为「内部状态(可共享)」和「外部状态(不可共享)」?
  • 享元模式的本质动作就是"分离内外状态",如果对象的状态几乎全部随环境变化(全部都是外部状态),或者状态之间耦合严重无法拆分,那么即使业务上对象再多,也没办法抽出共享部分。
  • 典型反例:每个订单对象都包含用户 ID、订单号、金额、时间戳、商品列表……这些数据每个订单都不同,几乎没有可共享部分。
  • 典型正例:棋子只有「颜色」是内部状态,「位置」是外部状态 → 干净利落地拆得开。

为什么排在第二? 因为只有先确认"业务上有需求",再去分析"技术上能不能实现"才有意义。这是一条技术侧的可行性条件。

第三道筛子:经济性 —— 真的「有内存压力」吗?

这是享元模式的性价比判断,决定"值不值得用"。

  • 问的是当前的资源代价:即使前两条都满足,引入享元也要付出代价(工厂、键计算、线程安全、状态分离带来的复杂度)。如果当前根本没有内存压力,那么"为了优化而优化"反而会让代码更难维护。
  • 典型场景:只有 1000 个对象、总占用不到 1MB → 即使可以享元,也没必要折腾。
  • 典型场景:100 万棵树木、每棵占 10KB → 内存压力显著,享元收益巨大。

为什么排在最后? 因为这是一道收益评估题。只有在确认"业务有需求"+"技术能实现"之后,再去衡量"投入产出比"才有意义。如果连前提都没有,谈收益就是空谈。

第四道筛子:实施细节 —— 共享对象能否保证「线程安全」?

这是享元模式的落地约束,决定"用了之后能不能稳稳定定地跑"。

  • 问的是编码条件:享元对象被多线程共享访问时,是否能保证状态一致且无并发问题?
  • 享元模式的核心是"多个调用方共享同一个对象",这天然引入并发风险:内部状态如果被修改,会污染所有调用方;工厂的池化过程如果没有并发控制,可能创建出重复对象。
  • 典型场景:
    • 只读共享(内部状态完全不可变)→ 无需额外处理
    • 可变内部状态 + 多线程 → 必须用 ConcurrentHashMap.computeIfAbsent 之类的原子操作,或对工厂加锁
    • 创建过程有副作用 → 同样需要同步机制

为什么排在最后? 因为前三道筛子回答的是"要不要用",第四道筛子回答的是"用了之后能不能稳住"。这是从决策走向落地的最后一关。

🧱 铁律(四筛之后的硬约束):通过这四道筛子后,内部状态必须完全不可变。一旦内部状态可写,多个调用方共享同一对象就会互相覆盖;此时就算加上锁,享元也丧失了"无锁共享"的最大收益。因此设计时就把内部状态设为 final、禁止 setter,是享元落地的前提——这条不可妥协。

5.1.3 四道筛子的逻辑关系总结

顺序 判据 维度 失败后果 通俗类比
1 大量相似对象 业务必要性 享元无对象可共享 修水库前先看有没有这条河
2 状态可外部化 技术可行性 拆不出共享部分 有河后看能不能筑坝
3 内存压力大 经济性 投入大于产出 筑坝前算算值不值
4 线程安全可控 实施细节 共享后并发问题无法收场 修好之后看会不会塌

5.1.4 常见疑问解答

疑问 1:为什么不是先问"内存压力大不大"?

因为内存压力是结果而不是原因。是否需要享元,根源在于"对象是否有共享可能性"和"业务量级是否达到需要共享",内存压力只是这两个条件满足后的一个可观察现象。倒过来问会出现这样的尴尬场景:明明业务上只有 5 个对象,硬因为"内存紧张"去享元,结果徒增复杂度。

疑问 2:第二个判据"大部分状态可外部化"——"大部分"具体指多少?

比例并非唯一指标。判定时应综合三要素:

  1. 可分离性:状态能否清晰地剥离为内部 + 外部?
  2. 不可变性:剥离出的内部状态是否不变?(把可变状态塞进共享对象会被互相改坏,触发线程安全问题)
  3. 复用度:剥离出的内部状态被多少调用方重复使用?

经验阈值(仅作参考):

  • 内部状态占对象总体大小的 50% 以上 → 典型复用场景下能带来显著收益
  • 内部状态不到 30% → 单看比例不划算,但若复用度极高(字符字形 1 万次、棋子规则 180 次)仍值得使用

判断的本质是:(内部状态大小 × 复用次数)应当远大于享元机制本身的开销(工厂、键、缓存查找)。

疑问 3:第三个判据如果选了"否",为什么是"可考虑但不必要"而不是"不适用"?

因为前两条都满足说明技术上完全可行,只是当前收益不显著。这种情况下:

  • 现状下不必上享元,避免过度设计
  • 但如果未来业务扩张(对象数量爆炸),可以平滑引入 → 它是一个可选的优化项

而前两个判据的"否"是结构性否定——没救,怎么改业务都救不回来,必须放弃享元思路。

疑问 4:第四道筛子过不了,是不是就完全不能用了?

不是。前三道筛子都通过、只是线程安全这一关过不了,仍然可以采用享元——只要加上同步机制(ConcurrentHashMap、锁、不可变设计等)。所以这一关的"否"路径是带条件的通过,而不是直接放弃。

一句话记忆:先看有没有量(业务必要),再看能不能拆(技术可行),然后算值不值(经济划算),最后看稳不稳(实施细节) —— 四层筛子缺一不可,顺序也不能颠倒。

5.1.5 实施要点速查

通过四大判据后,落地实施时建议按以下清单自检:

  1. 合理划分内部与外部状态:内部状态必须不可变(一旦可写就被多线程互相改坏);外部状态由调用方维护,且不应反向写回享元
  2. 工厂控制创建:使用工厂模式统一管理对象创建,避免重复对象
  3. 考虑线程安全:并发环境下使用 ConcurrentHashMap.computeIfAbsent 或加锁;详见后文"线程安全示例"
  4. 监控池大小:在工厂中暴露池大小,便于性能监控与调优
  5. 延迟初始化:结合懒加载按需创建对象,避免初始内存占用过大
  6. 关注池的内存滞留:工厂通常为 static/单例,会永久持有所有已创建的享元;需监控池大小是否单调上升,必要时提供 clear() 接口或采用弱引用池(注意会损失命中率)

5.1.6 性能评估指标

实施后可通过以下指标评估享元效果:

指标 评估方式
对象数量 享元池大小 vs 业务对象总数(比值越小越好)
内存占用 JVM 堆内存使用量对比
创建耗时 工厂 get 方法平均耗时
GC 频率 Young GC 触发频率对比
命中率 池中对象被复用次数 / 总请求次数

5.2 常见业务场景

场景 内部状态 外部状态
文字编辑器 字符内容、字体、字号 字符在文档中的位置
游戏场景 树木模型、贴图、纹理 树木坐标 X/Y
棋牌游戏 棋子颜色、形状 棋子在棋盘的位置
地图渲染 道路类型、材质 路段起点和终点
五子棋对战 棋子的黑白、形状 棋子的位置坐标
HTML 渲染 标签类型、CSS 样式 标签在 DOM 中的位置
GUI 控件库 图标外观、控件尺寸 控件坐标与父容器

六、不适用场景

  • 对象数量较少,内存压力不明显
  • 对象的大部分状态都是外部的,几乎没有可共享部分
  • 对象身份对业务至关重要(每个对象都必须唯一)
  • 共享对象会导致线程安全问题且代价巨大

七、优缺点分析

7.1 优点

mindmap root((享元模式优点)) 节省内存 减少对象数量 降低堆内存占用 减少 GC 频率 提升性能 对象创建开销降低 缓存命中后高效复用 可扩展 新增享元类方便 与组合模式结合使用 数据集中 内部状态集中管理 便于统一变更

7.2 缺点

  • 代码复杂度提升:需要分离内部状态与外部状态,增加了设计难度
  • 读取外部状态有成本:每次调用都需要传入外部状态
  • 线程安全问题:共享对象可能被多线程并发访问,需要考虑同步
  • 长时间持有:享元对象生命周期较长,需要注意内存泄漏

7.3 优缺点对比表

维度 优势 劣势
内存 大幅降低对象数量 享元对象长期驻留,需关注内存泄漏
性能 减少对象创建与 GC 多线程下需加锁或用并发集合
复杂度 抽象出公共部分 每次调用需传入外部状态
维护性 内部状态集中易修改 状态划分边界模糊,调试复杂

八、代码实现

8.1 示例一:经典实现 —— 享元棋子类

这是 GoF 经典案例,五子棋游戏中大量相同形状的棋子(黑子/白子)。

8.1.1 抽象享元

/**
 * 抽象享元角色 —— 棋子
 * 定义棋子的公共行为,需要接收外部状态(位置)
 */
public interface ChessPiece {
    /**
     * 落子操作
     * @param x 横坐标(外部状态)
     * @param y 纵坐标(外部状态)
     */
    void display(int x, int y);
}

8.1.2 具体享元

/**
 * 具体享元角色 —— 黑色棋子
 * 内部状态:颜色
 */
public class BlackChessPiece implements ChessPiece {

    private final String color = "黑色";

    @Override
    public void display(int x, int y) {
        System.out.printf("落子位置 (%d, %d),棋子颜色:%s%n", x, y, color);
    }
}
/**
 * 具体享元角色 —— 白色棋子
 * 内部状态:颜色
 */
public class WhiteChessPiece implements ChessPiece {

    private final String color = "白色";

    @Override
    public void display(int x, int y) {
        System.out.printf("落子位置 (%d, %d),棋子颜色:%s%n", x, y, color);
    }
}

8.1.3 享元工厂

import java.util.HashMap;
import java.util.Map;

/**
 * 享元工厂 —— 负责创建并缓存棋子对象
 */
public class ChessPieceFactory {

    // 享元池:缓存已创建的棋子
    private static final Map<String, ChessPiece> FLYWEIGHT_POOL = new HashMap<>();

    static {
        // 预创建两种颜色的棋子(游戏中只有黑/白两种共享棋子)
        FLYWEIGHT_POOL.put("black", new BlackChessPiece());
        FLYWEIGHT_POOL.put("white", new WhiteChessPiece());
    }

    /**
     * 获取棋子享元对象
     */
    public static ChessPiece getChessPiece(String color) {
        return FLYWEIGHT_POOL.get(color);
    }

    /**
     * 获取当前池中对象数量(用于验证享元效果)
     */
    public static int getPoolSize() {
        return FLYWEIGHT_POOL.size();
    }
}

8.1.4 客户端测试

public class FlyweightClient {

    public static void main(String[] args) {
        // 模拟一场五子棋:双方各下 100 步
        for (int i = 0; i < 100; i++) {
            // 黑子
            ChessPiece black = ChessPieceFactory.getChessPiece("black");
            black.display(i, i * 2);
            // 白子
            ChessPiece white = ChessPieceFactory.getChessPiece("white");
            white.display(i * 3, i + 5);
        }

        System.out.println("总共落子数:200");
        System.out.println("实际创建棋子对象数:" + ChessPieceFactory.getPoolSize());
    }
}

输出结果:

落子位置 (0, 0),棋子颜色:黑色
落子位置 (0, 5),棋子颜色:白色
...
落子位置 (99, 198),棋子颜色:黑色
落子位置 (297, 104),棋子颜色:白色
总共落子数:200
实际创建棋子对象数:2

✅ 效果:200 次落子操作仅创建了 2 个棋子对象,内存占用大幅降低。


8.2 示例二:复杂实现 —— 文字编辑器字符享元

模拟文字编辑器中字符对象的享元处理,这是享元模式最经典的应用。

本例中 CharacterFlyweight 同时承担"抽象享元接口"与"具体享元实现"两个角色(一个类包揽),是简化做法的常见写法,真实工程很常见。Document 内部用 CharPosition 记录每个字符的"行列"——这就是外部状态。

8.2.1 具体享元

/**
 * 具体享元 —— 字符
 * 内部状态:字符值、字体、字号、颜色
 */
public class CharacterFlyweight {

    private final char value;       // 字符本身(内部状态)
    private final String font;      // 字体(内部状态)
    private final int size;         // 字号(内部状态)
    private final String color;     // 颜色(内部状态)

    public CharacterFlyweight(char value, String font, int size, String color) {
        this.value = value;
        this.font = font;
        this.size = size;
        this.color = color;
    }

    /**
     * 渲染字符 —— 外部状态是字符在文档中的位置
     */
    public void render(int row, int column) {
        System.out.printf("在 [%d 行 %d 列] 渲染字符 %c,字体:%s,字号:%d,颜色:%s%n",
                row, column, value, font, size, color);
    }

    // 重写 equals 和 hashCode,用于工厂去重
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof CharacterFlyweight)) return false;
        CharacterFlyweight that = (CharacterFlyweight) o;
        return value == that.value
                && size == that.size
                && font.equals(that.font)
                && color.equals(that.color);
    }

    @Override
    public int hashCode() {
        int result = (int) value;
        result = 31 * result + font.hashCode();
        result = 31 * result + size;
        result = 31 * result + color.hashCode();
        return result;
    }
}

8.2.2 享元工厂

import java.util.HashMap;
import java.util.Map;

/**
 * 字符享元工厂 —— 基于内部状态去重
 */
public class CharacterFlyweightFactory {

    private static final Map<String, CharacterFlyweight> POOL = new HashMap<>();

    public static CharacterFlyweight get(char value, String font, int size, String color) {
        // 构造 key:内部状态组合
        String key = value + "-" + font + "-" + size + "-" + color;
        // 注意:此示例使用 HashMap,多线程下不安全;多线程安全版本见后文"线程安全示例"
        return POOL.computeIfAbsent(key,
                k -> new CharacterFlyweight(value, font, size, color));
    }

    public static int getPoolSize() {
        return POOL.size();
    }
}

8.2.3 客户端(文档)

import java.util.ArrayList;
import java.util.List;

/**
 * 文档类 —— 维护外部状态(每个字符的位置)
 */
public class Document {

    private final List<CharPosition> positions = new ArrayList<>();

    /**
     * 添加字符到文档中
     */
    public void addChar(char value, String font, int size, String color, int row, int col) {
        // 从工厂获取字符享元
        CharacterFlyweight flyweight = CharacterFlyweightFactory.get(value, font, size, color);
        positions.add(new CharPosition(flyweight, row, col));
    }

    /**
     * 渲染整个文档
     */
    public void render() {
        for (CharPosition cp : positions) {
            cp.flyweight.render(cp.row, cp.column);
        }
    }

    public int getCharCount() {
        return positions.size();
    }

    /**
     * 内部类:保存字符的位置(外部状态)
     */
    private static class CharPosition {
        final CharacterFlyweight flyweight;
        final int row;
        final int column;

        CharPosition(CharacterFlyweight flyweight, int row, int column) {
            this.flyweight = flyweight;
            this.row = row;
            this.column = column;
        }
    }
}

8.2.4 客户端测试

public class EditorClient {

    public static void main(String[] args) {
        Document doc = new Document();

        // 写入一段文本:Hello World!
        String text = "Hello World!";
        for (int i = 0; i < text.length(); i++) {
            doc.addChar(text.charAt(i), "宋体", 12, "黑色", 1, i + 1);
        }

        // 再写一行 Hello
        for (int i = 0; i < 5; i++) {
            doc.addChar(text.charAt(i), "宋体", 12, "黑色", 2, i + 1);
        }

        doc.render();

        System.out.println("\n文档字符总数:" + doc.getCharCount());
        System.out.println("享元池对象数:" + CharacterFlyweightFactory.getPoolSize());
    }
}

输出结果:

在 [1 行 1 列] 渲染字符 H,字体:宋体,字号:12,颜色:黑色
在 [1 行 2 列] 渲染字符 e,字体:宋体,字号:12,颜色:黑色
...
享元池对象数:9
文档字符总数:17

✅ 效果:17 个字符仅创建了 9 个享元对象(重复字符复用)。


8.3 示例三:线程安全的享元 —— 使用 ConcurrentHashMap

在多线程环境下,需要保证享元工厂的线程安全。

本节抽出一个泛型化的工厂 ThreadSafeFlyweightFactory<T>:可以套用前面任一具体享元类型,不必为每个具体类重写工厂。

8.3.1 线程安全的享元工厂

import java.util.concurrent.ConcurrentHashMap;

/**
 * 线程安全的享元工厂
 */
public class ThreadSafeFlyweightFactory<T> {

    private final ConcurrentHashMap<String, T> pool = new ConcurrentHashMap<>();

    /**
     * 获取享元对象(线程安全)
     * @param key 内部状态的 key
     * @param creator 当池中没有时用于创建新对象的函数
     */
    public T get(String key, java.util.function.Supplier<T> creator) {
        return pool.computeIfAbsent(key, k -> creator.get());
    }

    public int size() {
        return pool.size();
    }

    /**
     * 清理池(谨慎使用)
     */
    public void clear() {
        pool.clear();
    }
}

使用示例:

8.3.2 客户端测试

public class ThreadSafeExample {

    public static void main(String[] args) {
        ThreadSafeFlyweightFactory<CharacterFlyweight> factory = new ThreadSafeFlyweightFactory<>();

        // 模拟多线程并发获取享元
        Runnable task = () -> {
            for (int i = 0; i < 100; i++) {
                factory.get("H-宋体-12-黑色",
                        () -> new CharacterFlyweight('H', "宋体", 12, "黑色"));
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        Thread t3 = new Thread(task);

        t1.start();
        t2.start();
        t3.start();

        try {
            t1.join();
            t2.join();
            t3.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        System.out.println("并发访问后池中对象数:" + factory.size());
        // 无论多少线程访问,池中只有 1 个对象
    }
}

8.4 示例四:复合享元 —— 组合模式 + 享元模式

本例演示在树形结构中,叶子节点通过享元工厂共享,复合节点(树根)本身不共享。

🎯 本例是唯一演示全部 5 类角色的代码:前面的示例只有"抽象享元 / 具体享元 / 享元工厂 / 客户端"四类;本例多了不可共享享元 —— 即复合容器本身(不参与共享,但实现了同一个接口)。

📌 本例是组合模式的简化形态:仅实现了 add() 与 operation() 两个接口。完整的 Composite Pattern 还应提供 remove(CompositeFlyweight)、getChild(int)、isComposite() 等接口用于遍历与结构管理;本例为聚焦享元本质而省略这些。

8.4.1 抽象享元

/**
 * 抽象享元 —— 可包含子享元的复合节点
 */
interface CompositeFlyweight {
    void operation(String externalState);
}

8.4.2 具体享元(叶子节点)

⚠️ 注释陷阱:LeafFlyweight 容易被误标为"不可共享享元"——实际上它是共享的 ConcreteFlyweight,由后面的叶子工厂池化复用。原代码的注释写反了,此处已修正,请读者特别留意。

/**
 * 具体享元 —— 叶子节点(由后面的叶子工厂共享相同内部状态的实例)
 */
class LeafFlyweight implements CompositeFlyweight {

    private final String intrinsicState; // 内部状态

    public LeafFlyweight(String intrinsicState) {
        this.intrinsicState = intrinsicState;
    }

    @Override
    public void operation(String externalState) {
        System.out.println("叶子节点:内部状态=" + intrinsicState + ",外部状态=" + externalState);
    }
}

8.4.3 不可共享享元(复合容器)

import java.util.ArrayList;
import java.util.List;

/**
 * 不可共享享元 —— 复合容器节点(UnsharedConcreteFlyweight)
 * 注:复合容器本身不参与共享,但其叶子子节点由后面的叶子工厂池化复用
 */
class CompositeFlyweightImpl implements CompositeFlyweight {

    private final List<CompositeFlyweight> children = new ArrayList<>();

    public void add(CompositeFlyweight child) {
        children.add(child);
    }

    @Override
    public void operation(String externalState) {
        for (CompositeFlyweight child : children) {
            child.operation(externalState);
        }
    }
}

8.4.4 享元工厂(叶子工厂)

import java.util.HashMap;
import java.util.Map;

/**
 * 叶子享元工厂 —— 池化共享叶子节点
 */
class LeafFlyweightFactory {

    private static final Map<String, LeafFlyweight> POOL = new HashMap<>();

    public static LeafFlyweight get(String intrinsicState) {
        return POOL.computeIfAbsent(intrinsicState, LeafFlyweight::new);
    }

    public static int getPoolSize() {
        return POOL.size();
    }
}

8.4.5 客户端测试

public class CompositeFlyweightClient {

    public static void main(String[] args) {
        // 构造两棵树,复用相同类型的叶子节点
        CompositeFlyweightImpl tree1 = new CompositeFlyweightImpl();
        tree1.add(LeafFlyweightFactory.get("红色叶子"));
        tree1.add(LeafFlyweightFactory.get("绿色叶子"));
        tree1.add(LeafFlyweightFactory.get("红色叶子"));  // 复用

        CompositeFlyweightImpl tree2 = new CompositeFlyweightImpl();
        tree2.add(LeafFlyweightFactory.get("黄色叶子"));
        tree2.add(LeafFlyweightFactory.get("绿色叶子"));  // 复用
        tree2.add(LeafFlyweightFactory.get("红色叶子"));  // 复用

        tree1.operation("树一渲染");
        tree2.operation("树二渲染");

        System.out.println("叶子节点总数:6");
        System.out.println("享元池中实际对象数:" + LeafFlyweightFactory.getPoolSize());
        // 预期输出:3(红、绿、黄三种叶子)
    }
}

九、Java 标准库中的享元模式

9.1 Integer 缓存池

Java 中 Integer.valueOf(int) 使用了享元模式:

Integer a = Integer.valueOf(127);
Integer b = Integer.valueOf(127);
System.out.println(a == b);  // true(缓存命中,共享对象)

Integer c = Integer.valueOf(128);
Integer d = Integer.valueOf(128);
System.out.println(c == d);  // false(超出缓存范围,创建新对象)

IntegerCache 源码简化:

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

默认缓存范围是 -128 ~ 127,可通过 JVM 参数 -XX:AutoBoxCacheMax= 调整上限。

📌 本例是享元模式的简化形态:Integer 没有可分离的 extrinsic 状态(整数值本身就是全部信息),客户端也无须传入位置/颜色等上下文。这种"纯 intrinsic 池"是享元模式的一个退化形态。完整形态——即客户端必须传入 extrinsic 才能使用——见 §5.2 与 §8 中的字符 Glyph、棋类棋子与森林里的树(共享树模型/棋子规则,外部传入坐标/颜色/大小)。

9.2 String 常量池

String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2);  // true(共享常量池中的对象)

JVM 字符串常量池本质上就是一个享元池。

9.3 其他应用

类 内部状态 外部状态
Integer.valueOf() int 值 无
Long.valueOf() long 值 无
Boolean.valueOf() true/false 无
Byte.valueOf() byte 值 无
Character.valueOf() 0~127 的字符 无

十、与其他设计模式的对比

10.1 享元 vs 单例

维度 享元模式 单例模式
对象数量 多个共享对象(池) 仅一个全局对象
存在形式 工厂 + 池管理 类自身控制
状态 区分内部/外部状态 通常无状态或全状态
目的 节约内存 保证全局唯一性

10.2 享元 vs 对象池

维度 享元模式 对象池
对象生命周期 长期持有 用完归还
共享方式 同时共享给多个调用方 同一时刻独占
节省内存 是 主要节省创建时间
典型应用 字符、棋子 数据库连接、线程

10.3 享元 vs 缓存

维度 享元模式 缓存
目的 共享对象 加速访问
失效策略 通常不失效 LRU、TTL 等
对象获取 必须从工厂 可从缓存或源
命中率考量 重要(决定共享收益) 核心指标

十一、享元模式的实际框架应用

11.1 应用层次架构图

flowchart TB subgraph 应用层 A1[客户端代码] end subgraph 业务层 B1[享元工厂] B2[享元池] end subgraph 实现层 C1[具体享元 A] C2[具体享元 B] C3[具体享元 C] end A1 --> B1 B1 --> B2 B2 --> C1 B2 --> C2 B2 --> C3 style A1 fill:#FFE4B5 style B1 fill:#87CEEB style B2 fill:#98FB98

11.2 JDK 中的享元分布

pie title JDK 中享元模式应用占比示意 "Integer 缓存" : 35 "String 常量池" : 30 "Long/Short/Byte 缓存" : 20 "Boolean/Character 缓存" : 10 "其他" : 5

📌 数字依据提示:以上百分数是按"使用频度 × 公开可见性"的粗估——JDK 源码中出现频度高且开发者最常碰到的两类(Integer、String)合计占 65%。这是示意分布,并不来自任何官方文档;不同视角(例如按"复用次数 × 节省内存字节数")的分布会显著不同。


十二、总结

12.1 一句话概括

享元模式 = 共享内部状态 + 外部化可变状态 + 工厂池化缓存

12.2 模式要素回顾

mindmap root((享元模式要点)) 核心思想 共享技术 分离内外状态 关键角色 抽象享元 具体享元 享元工厂 应用价值 减少内存 提升性能 适合海量场景 注意事项 线程安全 状态划分 复杂度平衡

12.3 适用与不适用

适合使用 ✅ 不适合使用 ❌
文字编辑器字符对象 少量独立对象
游戏场景中重复元素 对象状态频繁变化
棋牌游戏中棋子 业务要求对象必须唯一
池化缓存场景 共享会导致严重线程问题

12.4 设计哲学

享元模式体现了软件设计中"以共享代替复制"的经典思想,是处理海量数据场景下性能与内存矛盾的有效武器。它提醒我们:在面对大规模对象时,识别不变性、分离可变性、引入复用层,往往是化繁为简的关键。

posted @ 2026-10-11 18:47  RK5123153  阅读(2)  评论(0)    收藏  举报