Skip to content

享元模式 (Flyweight Pattern)

一、定义

一句话概括:运用共享技术有效地支持大量细粒度对象的复用,通过共享减少内存占用。

官方定义(GoF):Use sharing to support large numbers of fine-grained objects efficiently.

享元模式的核心思想是:将对象的内部状态(Intrinsic State,可共享的、不随环境变化的部分)和外部状态(Extrinsic State,不可共享的、随环境变化的部分)分离。内部状态存储在享元对象内部,可以共享;外部状态由客户端在调用时传入。


二、解决的问题

2.1 什么场景下需要享元模式?

  • 大量相似对象:系统中存在大量细粒度对象,且这些对象的大部分状态可以共享。
  • 内存瓶颈:对象数量过多导致内存溢出或性能下降。
  • 对象创建开销大:对象的创建和销毁代价高昂,需要复用。
  • 缓存复用:需要缓存和复用已经创建的对象。

2.2 不用享元模式会有什么问题?

假设一个文本编辑器,每个字符都是一个对象(包含字体、大小、颜色、位置等属性)。一篇文章有 10 万个字符:

问题1:内存爆炸 —— 100,000 个字符对象,每个 100 字节,总内存 ≈ 10MB
问题2:GC 压力 —— 创建和销毁大量对象,频繁触发 GC
问题3:创建开销 —— 每个字符对象都需要初始化,耗时
问题4:缓存失效 —— 无法复用相同字体、大小的字符对象

如果用享元模式,26 个字母 + 标点 ≈ 100 个享元对象,每个对象被共享,内存从 10MB 降到 10KB。


三、结构

3.1 文字描述

享元模式包含以下角色:

  • Flyweight(抽象享元):定义享元接口,接受外部状态。
  • ConcreteFlyweight(具体享元):实现享元接口,存储内部状态(可共享)。
  • UnsharedConcreteFlyweight(非共享具体享元):不需要共享的享元实现。
  • FlyweightFactory(享元工厂):创建和管理享元对象,维护享元池。
  • Client(客户端):维护外部状态,通过工厂获取享元对象。

3.2 ASCII 类图

┌──────────────────────┐     ┌────────────────────────┐
│   FlyweightFactory   │     │  <<interface>>          │
├──────────────────────┤     │    Flyweight            │
│ - pool: Map<K, FW>   │     ├────────────────────────┤
│ + getFlyweight(key)  │────>│ + operation(extState)   │
│   检查池中是否存在     │     └───────────┬────────────┘
│   存在则返回,否则创建  │                │
└──────────────────────┘     ┌────────────┴────────────┐
                             │                         │
                    ┌────────┴──────────┐   ┌──────────┴──────────┐
                    │ ConcreteFlyweight  │   │UnsharedFlyweight    │
                    ├───────────────────┤   ├─────────────────────┤
                    │ - intrinsicState   │   │ - allState          │
                    │ + operation(ext)   │   │ + operation(ext)    │
                    └───────────────────┘   └─────────────────────┘

    内部状态(intrinsicState):存储在享元内部,可共享,不随环境变化
    外部状态(extrinsicState):由客户端传入,随环境变化,不可共享

四、代码实现

4.1 基础实现

场景:棋盘游戏中的棋子

java
// ============ Flyweight:抽象享元 ============
interface ChessPiece {
    void display(int x, int y);  // x, y 是外部状态
}

// ============ ConcreteFlyweight:具体享元 ============
class ConcreteChessPiece implements ChessPiece {
    private String color;   // 内部状态:颜色(可共享)
    private String shape;   // 内部状态:形状(可共享)

    public ConcreteChessPiece(String color, String shape) {
        this.color = color;
        this.shape = shape;
        System.out.println("创建棋子: " + color + " " + shape);  // 只打印一次
    }

    @Override
    public void display(int x, int y) {
        // x, y 是外部状态:位置(不可共享,每次变化)
        System.out.println(color + " " + shape + " 显示在 (" + x + ", " + y + ")");
    }
}

// ============ FlyweightFactory:享元工厂 ============
class ChessPieceFactory {
    private static final Map<String, ChessPiece> pool = new HashMap<>();

    public static ChessPiece getChessPiece(String color, String shape) {
        String key = color + "_" + shape;
        if (!pool.containsKey(key)) {
            pool.put(key, new ConcreteChessPiece(color, shape));
        }
        return pool.get(key);
    }

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

// ============ 客户端测试 ============
public class FlyweightDemo {
    public static void main(String[] args) {
        // 下棋:10 步操作,但只需要 2 个棋子对象
        ChessPiece blackKnight = ChessPieceFactory.getChessPiece("黑", "马");
        ChessPiece whiteKnight = ChessPieceFactory.getChessPiece("白", "马");

        // 复用同一个棋子对象,只是位置不同
        blackKnight.display(1, 2);
        blackKnight.display(3, 4);
        blackKnight.display(5, 6);
        whiteKnight.display(2, 3);
        whiteKnight.display(4, 5);

        System.out.println("享元池大小: " + ChessPieceFactory.getPoolSize());
        // 输出:创建棋子: 黑 马
        //       创建棋子: 白 马
        //       黑 马 显示在 (1, 2)
        //       黑 马 显示在 (3, 4)
        //       黑 马 显示在 (5, 6)
        //       白 马 显示在 (2, 3)
        //       白 马 显示在 (4, 5)
        //       享元池大小: 2
    }
}

4.2 进阶实现

4.2.1 内部状态 vs 外部状态

java
// ============ 文本编辑器字符享元 ============

// 字符享元(内部状态:字符值、字体、大小、颜色)
class CharacterFlyweight {
    private final char value;        // 内部状态
    private final String font;       // 内部状态
    private final int fontSize;      // 内部状态
    private final String color;      // 内部状态

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

    // 外部状态:行号、列号(由客户端传入)
    public void render(int row, int column) {
        System.out.printf("渲染字符 '%c' [%s, %dpt, %s] 在 (%d, %d)%n",
            value, font, fontSize, color, row, column);
    }
}

// 享元工厂
class CharacterFactory {
    private static final Map<String, CharacterFlyweight> cache = new HashMap<>();

    public static CharacterFlyweight getCharacter(char c, String font, int size, String color) {
        String key = c + "_" + font + "_" + size + "_" + color;
        return cache.computeIfAbsent(key,
            k -> new CharacterFlyweight(c, font, size, color));
    }

    public static int getCacheSize() {
        return cache.size();
    }
}

// 客户端:渲染一篇文章
class TextEditor {
    // 每个字符位置保存享元引用 + 外部状态
    static class CharPosition {
        CharacterFlyweight flyweight;
        int row, column;
    }

    private List<CharPosition> document = new ArrayList<>();

    public void addCharacter(char c, String font, int size, String color, int row, int col) {
        CharPosition pos = new CharPosition();
        pos.flyweight = CharacterFactory.getCharacter(c, font, size, color);
        pos.row = row;
        pos.column = col;
        document.add(pos);
    }

    public void render() {
        for (CharPosition pos : document) {
            pos.flyweight.render(pos.row, pos.column);
        }
    }
}

// 测试:10000 个字符,但只需要 26 个享元对象
// TextEditor editor = new TextEditor();
// for (int i = 0; i < 10000; i++) {
//     editor.addCharacter((char)('A' + i % 26), "宋体", 12, "黑色", i / 80, i % 80);
// }
// System.out.println("享元对象数: " + CharacterFactory.getCacheSize()); // 26

4.2.2 连接池(享元模式变体)

java
// ============ 数据库连接池 ============
class ConnectionPool {
    private final List<Connection> pool;
    private final int maxSize;
    private int currentSize = 0;

    public ConnectionPool(int maxSize) {
        this.maxSize = maxSize;
        this.pool = new ArrayList<>(maxSize);
    }

    // 获取连接
    public synchronized Connection getConnection() {
        if (!pool.isEmpty()) {
            System.out.println("从池中复用连接");
            return pool.remove(pool.size() - 1);
        }
        if (currentSize < maxSize) {
            System.out.println("创建新连接");
            currentSize++;
            return new Connection("Conn-" + currentSize);
        }
        throw new RuntimeException("连接池已满,等待释放");
    }

    // 归还连接
    public synchronized void releaseConnection(Connection conn) {
        if (conn != null) {
            pool.add(conn);
            System.out.println("归还连接: " + conn.getName());
        }
    }
}

class Connection {
    private String name;
    public Connection(String name) { this.name = name; }
    public String getName() { return name; }
    public void execute(String sql) { System.out.println(name + " 执行: " + sql); }
}

4.2.3 Integer 缓存(JDK 享元模式)

java
// JDK 源码中 Integer.valueOf() 使用了享元模式
// IntegerCache 缓存了 -128 到 127 的 Integer 对象

public class IntegerCacheDemo {
    public static void main(String[] args) {
        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 —— 超出缓存范围,创建新对象

        // 这就是享元模式:-128~127 之间的 Integer 对象被共享
        // 注意:Integer 的内部状态 value 是 final 的,不可变
    }
}

4.3 生产级实现

Spring Boot 缓存 + 享元模式

java
// ============ 商品 SKU 享元 ============
// 场景:电商系统中,同一个 SKU 的商品信息被大量订单引用

@Component
class SkuFlyweightFactory {
    // 一级缓存:本地内存
    private final Map<Long, SkuFlyweight> localCache = new ConcurrentHashMap<>();

    @Autowired
    private SkuRepository skuRepository;

    // 获取 SKU 享元(只读、共享)
    public SkuFlyweight getSku(Long skuId) {
        return localCache.computeIfAbsent(skuId, id -> {
            SkuEntity entity = skuRepository.findById(id)
                .orElseThrow(() -> new BusinessException("SKU 不存在: " + id));
            return new SkuFlyweight(entity);
        });
    }

    // 刷新缓存(商品信息变更时调用)
    public void refresh(Long skuId) {
        localCache.remove(skuId);
    }

    public int getCacheSize() {
        return localCache.size();
    }
}

// 享元对象:只包含内部状态(可共享的商品属性)
class SkuFlyweight {
    private final Long id;           // 内部状态
    private final String name;       // 内部状态
    private final BigDecimal price;  // 内部状态
    private final String imageUrl;   // 内部状态
    private final String category;   // 内部状态

    public SkuFlyweight(SkuEntity entity) {
        this.id = entity.getId();
        this.name = entity.getName();
        this.price = entity.getPrice();
        this.imageUrl = entity.getImageUrl();
        this.category = entity.getCategory();
    }

    // 外部状态(数量)由客户端传入
    public OrderItemInfo createOrderItem(int quantity) {
        BigDecimal totalPrice = price.multiply(BigDecimal.valueOf(quantity));
        return new OrderItemInfo(id, name, price, quantity, totalPrice, imageUrl);
    }

    public Long getId() { return id; }
    public String getName() { return name; }
    public BigDecimal getPrice() { return price; }
}

// 订单项信息(包含外部状态)
class OrderItemInfo {
    private Long skuId;
    private String skuName;
    private BigDecimal unitPrice;
    private int quantity;
    private BigDecimal totalPrice;
    private String imageUrl;

    public OrderItemInfo(Long skuId, String skuName, BigDecimal unitPrice,
                         int quantity, BigDecimal totalPrice, String imageUrl) {
        this.skuId = skuId;
        this.skuName = skuName;
        this.unitPrice = unitPrice;
        this.quantity = quantity;
        this.totalPrice = totalPrice;
        this.imageUrl = imageUrl;
    }
}

// 在订单服务中使用
@Service
class OrderService {
    @Autowired
    private SkuFlyweightFactory skuFactory;

    public OrderResult createOrder(OrderRequest request) {
        List<OrderItemInfo> items = new ArrayList<>();
        for (OrderItem item : request.getItems()) {
            // 获取享元(共享的商品信息)
            SkuFlyweight sku = skuFactory.getSku(item.getSkuId());
            // 传入外部状态(数量)
            items.add(sku.createOrderItem(item.getQuantity()));
        }
        // ... 创建订单逻辑
        return new OrderResult(items);
    }
}

五、优缺点

优点

优点说明
大幅节省内存共享相同状态的对象,减少内存占用
减少 GC 压力对象数量减少,GC 频率降低
提高性能复用已有对象,避免重复创建的开销

缺点

缺点说明
复杂度增加需要分离内部状态和外部状态,设计复杂
线程安全享元对象被多线程共享,需要保证线程安全(通常设计为不可变对象)
外部状态管理外部状态由客户端维护,增加了客户端的负担
不适合所有场景如果对象状态差异很大,无法共享,享元模式没有收益

六、适用场景

  1. 大量相似对象:文本编辑器中的字符、游戏中的粒子、地图中的瓦片。
  2. 缓存系统:字符串常量池、Integer 缓存、数据库连接池、线程池。
  3. 数据字典:系统中的枚举值、状态码、配置项等。
  4. 图形处理:图形编辑器中的形状、颜色、笔刷等。
  5. 电商系统:商品 SKU、优惠券模板等被大量引用的只读对象。

七、JDK / Spring 框架中的实际应用

7.1 JDK 中的享元模式

java
// 1. String 常量池
String s1 = "hello";     // 从常量池获取
String s2 = "hello";     // 复用常量池中的对象
System.out.println(s1 == s2);  // true

// 2. Integer 缓存 (-128 ~ 127)
Integer i1 = Integer.valueOf(100);
Integer i2 = Integer.valueOf(100);
System.out.println(i1 == i2);  // true

// 3. Boolean 常量
Boolean b1 = Boolean.valueOf(true);
Boolean b2 = Boolean.valueOf(true);
System.out.println(b1 == b2);  // true —— Boolean.TRUE 和 Boolean.FALSE 是静态常量

// 4. Byte, Short, Long 缓存 (-128 ~ 127)
// 5. Character 缓存 (0 ~ 127)

7.2 Spring 中的享元模式

java
// 1. Spring Bean 单例作用域
// 默认 Singleton 的 Bean 可以被视为享元对象
// 所有注入该 Bean 的地方共享同一个实例

// 2. Spring 的 ConversionService 中的 Converter 缓存
// 3. Spring 的 PropertyEditor 注册表
// 4. Hibernate 的一级缓存 / 二级缓存

八、与其他模式的关系

模式关系
组合模式组合模式中的叶子节点可以使用享元模式共享,减少内存占用。
单例模式享元工厂通常是单例的,享元对象是多例但可共享的。享元模式可以看作单例模式的扩展(多例共享)。
工厂模式享元工厂通常使用工厂方法创建和管理享元对象。
状态模式享元模式中的内部状态类似状态模式中的共享状态,但意图不同。
原型模式两者都可以减少对象创建开销,但享元通过共享,原型通过克隆。

九、面试常见问题

Q1:什么是内部状态和外部状态?如何区分?

  • 内部状态(Intrinsic State):存储在享元对象内部,不随环境变化,可以共享。例如:字符的字体、大小、颜色。
  • 外部状态(Extrinsic State):随环境变化,不可共享,由客户端在调用时传入。例如:字符在文档中的行号、列号。
  • 区分原则:问自己"这个状态是否可以被多个客户端共享?" 能共享的就是内部状态,不能共享的就是外部状态。

Q2:享元模式和单例模式有什么区别?

  • 单例模式:一个类只有一个实例,全局唯一。
  • 享元模式:一个类可以有多个实例,但每个实例被多个客户端共享。享元模式可以看作"多例模式"——每个唯一键对应一个实例。
  • 享元模式关注的是"大量相似对象的复用",单例模式关注的是"全局唯一实例"。

Q3:享元模式如何保证线程安全?

:最推荐的方式是将享元对象设计为不可变对象(Immutable)。所有字段声明为 final,不提供 setter 方法,这样天然线程安全。例如 JDK 中的 IntegerString 都是不可变的。如果享元对象必须可变,则需要使用并发控制(如 synchronizedReentrantLockConcurrentHashMap)。

Q4:JDK 中 Integer 的缓存范围是多少?为什么是 -128 到 127?

Integer 默认缓存范围是 -128 到 127(可以通过 -XX:AutoBoxCacheMax 调整上限)。选择这个范围是因为 -128 到 127 涵盖了一个字节的表示范围,是最常用的整数范围。这是典型的享元模式——用空间换时间,用少量内存缓存常用对象,避免重复创建。

Q5:享元模式在什么情况下反而会降低性能?

  1. 对象状态差异很大时,缓存命中率低,享元工厂的查找开销反而成为负担。
  2. 享元对象很少被复用时,维护缓存池的代价大于创建新对象的代价。
  3. 外部状态过多时,客户端管理外部状态的复杂度增加。
  4. 享元对象是可变的时,需要额外的同步开销保证线程安全。