一、享元模式:对象复用,线程池与连接池中的享元模式

享元模式,说白了就是「共享对象」。

我刚开始接触这个模式时,觉得它挺简单的——不就是复用对象嘛。但真正在项目中用起来,才发现里面的门道不少。你想想看,如果一个系统里创建了成千上万个相同的对象,内存迟早会爆。享元模式就是来解决这个问题的。

1.1 什么是享元模式

享元模式的核心思想是:通过共享技术,减少大量细粒度对象的创建

它把对象的状态分为两类:

  • 内部状态:可共享的、不变的部分。比如字符的字体、颜色。
  • 外部状态:不可共享的、随上下文变化的部分。比如字符的位置、大小。

嗯,这里要注意:内部状态必须是不变的。如果变了,共享就出问题了。

核心要点:享元模式通过「共享内部状态 + 传入外部状态」来减少对象数量。

1.2 一个简单的例子

我记得有一次做文本编辑器,需要显示大量字符。如果每个字符都创建一个对象,那内存根本扛不住。用享元模式就解决了。

// 享元接口
public interface Character {
    void display(int fontSize, String color);
}

// 具体享元类
public class ConcreteCharacter implements Character {
    private final char symbol;  // 内部状态,不可变

    public ConcreteCharacter(char symbol) {
        this.symbol = symbol;
    }

    @Override
    public void display(int fontSize, String color) {
        System.out.println("字符: " + symbol + 
                           ", 字号: " + fontSize + 
                           ", 颜色: " + color);
    }
}

// 享元工厂
public class CharacterFactory {
    private final Map<Character, ConcreteCharacter> pool = new HashMap<>();

    public ConcreteCharacter getCharacter(char key) {
        ConcreteCharacter character = pool.get(key);
        if (character == null) {
            character = new ConcreteCharacter(key);
            pool.put(key, character);
            System.out.println("创建新字符: " + key);
        }
        return character;
    }
}

你看,同一个字符 'A' 只会被创建一次。外部状态(字号、颜色)每次传入即可。

个人经验:我在项目中习惯用 HashMap 做享元池,但要注意线程安全。如果并发高,建议用 ConcurrentHashMap。

1.3 享元模式的结构

享元模式的结构其实很清晰:

  • Flyweight(享元接口):定义对外接口,接收外部状态。
  • ConcreteFlyweight(具体享元):实现享元接口,存储内部状态。
  • FlyweightFactory(享元工厂):创建和管理享元对象。
  • Client(客户端):维护外部状态,调用享元对象。
享元模式结构图 Client(客户端) FlyweightFactory getFlyweight(key) Flyweight(接口) operation(extrinsicState) ConcreteFlyweight

1.4 线程池中的享元模式

线程池是享元模式最经典的应用之一。我做过一个高并发项目,如果不复用线程,每来一个请求就 new 一个线程,系统很快就挂了。

ExecutorService executor = Executors.newFixedThreadPool(10);
// 这10个线程会被反复使用,而不是每次都创建新线程

线程池的工作原理:

  • 内部维护一个线程集合(享元池)
  • 任务提交时,从池中取一个空闲线程执行
  • 执行完毕后,线程不销毁,而是回到池中等待下一个任务

避坑指南:我曾经在项目中把线程池设得太大,结果内存溢出。后来我习惯根据 CPU 核心数和任务类型来估算线程数。一般 IO 密集型任务可以多设一些,CPU 密集型任务不宜超过核心数。

1.5 连接池中的享元模式

数据库连接池也是享元模式的典型应用。每次操作数据库都创建连接,那性能太差了。

// 以 HikariCP 为例
HikariConfig config = new HikariConfig();
config.setMaximumPoolSize(20);
HikariDataSource dataSource = new HikariDataSource(config);

// 从池中获取连接
Connection conn = dataSource.getConnection();
// 使用连接...
conn.close();  // 这里不是真正关闭,而是归还到池中

连接池的好处:

  • 避免频繁创建/销毁连接的开销
  • 控制并发连接数,防止数据库被打爆
  • 提供连接超时、重连等机制

个人经验:我建议连接池的大小不要设置成固定的,可以根据实际负载动态调整。比如用 HikariCP 的 minimumIdle 和 maximumPoolSize 配合使用。

1.6 享元模式的优缺点

优点 缺点
减少对象数量,节省内存 需要分离内部状态和外部状态,增加复杂度
提高系统性能 如果外部状态变化频繁,反而降低性能
便于管理共享资源 享元工厂可能成为瓶颈

1.7 什么时候用享元模式

我个人总结了几条经验:

  • 系统中有大量相似对象,且内存消耗严重
  • 对象的大部分状态可以外部化
  • 需要复用对象,且对象创建成本高
  • 需要控制资源数量(如线程、连接)

核心总结:享元模式不是万能的。如果对象状态变化频繁,或者共享带来的收益不大,就别硬用。我见过有人为了用设计模式而用设计模式,结果代码变得很复杂,性能反而下降了。

嗯,享元模式就讲到这里。记住一句话:能复用就别创建,能共享就别独享。线程池、连接池都是这个道理。


公众号:蓝海资料掘金营,微信deep3321