Java 23 种设计模式:从踩坑到精通 | 番外:享元 vs 对象池 —— 不可变共享 vs 可变复用
Java 23 种设计模式:从踩坑到精通 | 番外:享元 vs 对象池 —— 不可变共享 vs 可变复用
摘要:享元模式和对象池模式都能减少对象创建、节约内存,但享元的核心是不可变对象的共享——相同内部状态的客户端拿到的是同一个只读实例;对象池的核心是可变对象的复用——客户端借出、用完归还,对象状态随时变化。本文从字符共享和数据库连接池两个场景出发,结合代码对比、UML 分析和
Integer.valueOf()、HikariCP 等 JDK 源码,帮你彻底分清这对“池化型”模式。
🗺️ 本文阅读地图(3 分钟速览)
- 为什么字符共享是享元,连接池是对象池?
- UML 对比:不可变共享 vs 借还复用
- 手写享元工厂 vs 对象池,看代码感受差异
- JDK
Integer.valueOf()(享元)vs 数据库连接池(对象池)源码分析- 面试必问:“享元和对象池怎么区分?连接池是享元吗?”
📖 《Java 23 种设计模式:从踩坑到精通》
开篇:系列介绍与目录 | 当前:番外 · 享元 vs 对象池
🔗 返回系列总目录
文章目录
1. 从“字符共享”和“连接池”两个场景说起
1.1 场景一:文本编辑器字符共享(享元模式)
一篇十万字的文章,如果每个字符都 new 一个 Character 对象,内存会迅速撑爆。享元模式的思路:将字符对象的状态分为内部状态(字符、字体、字号——不可变、可共享)和外部状态(行列位置——由客户端管理)。享元工厂维护一个池,每种内部状态只创建一个实例。
// 享元工厂:每种内部状态只创建一个不可变实例
public class CharacterFactory {
private static final Map<String, TextCharacter> pool = new ConcurrentHashMap<>();
public static TextCharacter getCharacter(char symbol, String font, int size) {
String key = symbol + "_" + font + "_" + size;
return pool.computeIfAbsent(key, k -> new ConcreteCharacter(symbol, font, size));
}
}
// 客户端:11 个字符只创建了 8 个享元实例(l 和 o 重复)
TextCharacter c1 = CharacterFactory.getCharacter('l', "Arial", 12); // 新创建
TextCharacter c2 = CharacterFactory.getCharacter('l', "Arial", 12); // 复用!和 c1 是**同一个对象**
System.out.println(c1 == c2); // true
💬 白话:享元池中的对象是“只读”的——创建后就不能修改。客户端只是拿来用,用完不用归还。多个客户端拿到的是同一个实例。
1.2 场景二:数据库连接池(对象池模式)
数据库连接对象是有状态、可变的。一个连接可能处于“空闲”、“已借用”、“已关闭”等状态。对象池的运作方式:客户端从池中借一个空闲连接,用完后归还,池负责管理连接的生命周期。
// HikariCP 对象池(简化示例)
HikariDataSource dataSource = new HikariDataSource(config);
// 客户端1:借一个连接
Connection conn1 = dataSource.getConnection(); // 从池中借出
conn1.setAutoCommit(false); // 修改连接状态
// ... 执行业务操作 ...
conn1.close(); // 归还给池
// 客户端2:借一个连接(可能是同一个物理连接,但状态已重置)
Connection conn2 = dataSource.getConnection(); // 从池中借出
System.out.println(conn1 == conn2); // false,不同对象(或状态已重置的同一对象)
💬 白话:对象池中的对象是“借出-归还”模式。借出时可能被修改状态,归还时池会重置状态(如回滚事务、清理上下文)。多个客户端拿到的是不同的实例(或状态已被重置的实例)。
1.3 关键差异已浮现
- 享元:对象创建后就不可变,多个客户端共享同一个实例,不用归还;
- 对象池:对象可变,客户端借出→使用→归还,池负责状态管理。
2. UML 对比

3. 三大核心区别
| 对比维度 | 享元模式 | 对象池模式 |
|---|---|---|
| 对象可变性 | 不可变(创建后不能修改) | 可变(借出后状态可能被修改) |
| 共享方式 | 相同 key 返回同一个实例(== 为 true) |
借出不同的实例,用完归还 |
| 生命周期管理 | 常驻内存,一般不销毁 | 借出→使用→归还→可销毁(空闲超时) |
| 典型应用 | Integer.valueOf(-128~127)、字符串常量池 |
数据库连接池(HikariCP)、线程池 |
| 核心特征 | 只读共享,外部状态由客户端管理 | 复用归还,状态由池管理 |
4. JDK 源码深度对比
4.1 Integer.valueOf()(享元模式)
// Integer 对 -128~127 范围的整数进行了缓存
// 范围内的整数共享同一个不可变 Integer 实例
Integer a = Integer.valueOf(100);
Integer b = Integer.valueOf(100);
System.out.println(a == b); // true,同一个对象!
Integer c = Integer.valueOf(200);
Integer d = Integer.valueOf(200);
System.out.println(c == d); // false,超出缓存范围,新建对象
为什么是享元?
Integer对象不可变(final class,没有 setter);- 相同值的整数返回同一个实例(在缓存范围内);
- 客户端只是“拿来用”,用完不需要归还;
- 内部状态(整数值)是可共享的,外部状态(无)由客户端管理。
4.2 HikariCP 数据库连接池(对象池模式)
// HikariCP 是典型的对象池模式
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
config.setMaximumPoolSize(10); // 最大连接数
config.setIdleTimeout(600000); // 空闲超时 10 分钟
HikariDataSource dataSource = new HikariDataSource(config);
// 借出连接
Connection conn = dataSource.getConnection();
// 使用连接(可能修改状态:设置事务隔离级别、超时等)
conn.setAutoCommit(false);
// 归还连接(由池重置状态)
conn.close();
为什么是对象池?
Connection对象可变(状态包括:是否自动提交、事务隔离级别、超时等);- 每次
getConnection()返回的是不同实例或状态已被重置的实例; - 客户端必须归还(调用
close()归还给池,而非真正关闭); - 池管理对象生命周期(最大连接数、空闲超时、连接验证)。
5. 什么时候不该用?
| 滥用场景 | 应该用什么 | 原因 |
|---|---|---|
| 对象状态需要频繁修改(如连接) | 对象池 | 享元要求对象不可变,状态可变会导致数据污染 |
| 对象创建成本低,数量不大 | 直接 new | 池化增加代码复杂度,收益不大 |
| 对象不可变且只有少量种类 | 享元 | 对象池管理复杂度高,不需要借还逻辑 |
| 线程复用 | 线程池(对象池变体) | 线程是有状态、可变的,不是不可变共享 |
6. 面试必问 + 追问连环炮
基础必问
- 享元和对象池有什么区别? → 享元共享不可变对象(相同 key 返回同一实例),对象池复用可变对象(借出-归还模式)。
- 数据库连接池是享元模式吗? → 不是。连接池是对象池模式。连接对象有状态、可变,需要借出和归还。
面试官追问
- “
Integer.valueOf()是享元模式,那String.intern()呢?”
👉 也是享元模式。intern()将字符串放入常量池,相同内容的字符串共享同一个不可变实例。 - “对象池中的对象归还时为什么要重置状态?”
👉 防止上一个使用者的状态污染下一个使用者。例如归还连接时需要回滚未提交的事务、清除临时表、重置隔离级别。 - “如果享元对象的内部状态被意外修改了会怎样?”
👉 数据污染事故。A 客户端修改了共享对象,B 客户端拿到的是脏数据。因此享元对象必须设计为不可变(final字段 + 无 setter),这是享元模式最致命的踩坑点。 - “线程池是享元还是对象池?”
👉 对象池。线程是有状态的(运行/阻塞/等待),借出后状态不断变化,用完归还回池中。ThreadLocal 可以用来给每个线程分配独立的上下文,但线程本身是对象池管理的。
🎉 恭喜:如果你能立刻说出
Integer.valueOf()是享元(不可变共享)、数据库连接池是对象池(可变复用),并理解“享元对象必须不可变”的原因,你已经掌握了这对“池化型”模式的核心区分点。
💡 延伸阅读:本文是享元模式与对象池模式的深度对比。如果你想先了解享元模式的完整原理、UML、代码实现和面试题,可以先阅读正篇——享元模式:内存吃不消?试试共享细粒度对象,再回来看这篇对比,效果更佳。
🧭 《Java 23 种设计模式:从踩坑到精通》快速导航
- 开篇:系列介绍与目录
- 当前:番外 · 享元 vs 对象池 —— 不可变共享 vs 可变复用(你在这里)
- 📖 延伸阅读:享元模式正篇
- 创建型模式汇总
- 结构型模式汇总
- 行为型模式汇总
🔔 关注《Java 23 种设计模式:从踩坑到精通》,用 25 篇文章彻底吃透设计模式。
📦 福利预告:全系列代码及 UML 源码将在完结时统一打包开放,点击「关注」「收藏」第一时间获取。
📌 除了设计模式,我也在深挖智能物流实战(WMS、托盘调度、机器学习落地)。欢迎点击头像,看看专栏 《出版社物流WMS智能调度实战》。技术相通,思路可鉴。
更多推荐





所有评论(0)