这是 2020 年 6 月整理的设计模式专题第九篇。享元模式是”以共享换性能”的代表——大量相似对象重复创建很浪费内存,共享起来就省了。Java 的 Integer 缓存、字符串常量池、连接池,都是它的应用。
简单说明
享元模式(Flyweight Pattern):提供了减少对象的数量从而改善应用所需的对象结构的方式,运用共享技术有效地支持大量细粒度的对象。
- 常常应用于系统底层开发,以便解决系统的性能问题;
- 或者系统中有大量相似对象,需要缓冲池的场景。
内部状态与外部状态
享元模式最关键的概念是状态分离:
| 状态 | 含义 | 示例 |
|---|---|---|
| 内部状态 | 存储在享元对象内部,可共享、不随环境变化 | 棋子的形状、颜色 |
| 外部状态 | 由客户端传入,随场景变化,不可共享 | 棋子的位置坐标 |
核心思想:把可共享的内部状态放进享元对象缓存复用;把不可共享的外部状态在使用时由客户端传入,不存入共享对象。
/**
* 描述:享元接口
*/
public interface Chess {
void play(int x, int y); // x,y 是外部状态,由客户端传入
}
/**
* 描述:具体享元:棋子(内部状态:颜色)
*/
public class ChessPiece implements Chess {
private String color; // 内部状态:可共享
public ChessPiece(String color) {
this.color = color;
}
@Override
public void play(int x, int y) {
System.out.println(color + "棋落在(" + x + "," + y + ")");
}
}
/**
* 描述:享元工厂:用 Map 缓存,复用对象
*/
public class ChessFactory {
private static final Map<String, Chess> CHESS_MAP = new HashMap<>();
public static Chess getChess(String color) {
// 缓存中已有就直接复用,没有才创建
Chess chess = CHESS_MAP.get(color);
if (chess == null) {
chess = new ChessPiece(color);
CHESS_MAP.put(color, chess);
}
return chess;
}
}
public class FlyweightTest {
public static void main(String[] args) {
// 一盘棋 32 颗棋子,但只有黑白 2 个享元对象,位置由外部传入
Chess black1 = ChessFactory.getChess("黑");
Chess black2 = ChessFactory.getChess("黑");
System.out.println("black1 == black2 : " + (black1 == black2)); // true,同一个对象
black1.play(1, 1);
black2.play(2, 2);
}
}
关键:black1 == black2 为 true——同样的”黑棋”对象被反复复用,只有位置(外部状态)在变。
优点与缺点
优点:
- 减少对象的创建,降低内存中对象数量,降低系统内存,提高效率;
- 减少内存之外其他资源的占用(如数据库连接、线程等昂贵资源)。
缺点:
- 需要关注内外部状态的划分,关注线程安全问题(共享对象被并发修改);
- 使系统、程序逻辑复杂化。
实现要点
将对象创建后,用例如 Map 结构缓存起来,再用的时候直接从缓存容器中获取。例如各种连接池功能的实现。
public class ConnectionPool {
private static final List<Connection> POOL = new LinkedList<>();
// 初始化时创建固定数量连接放入池中
// 取用:从池中借出;用完:归还到池中
}
现实中的应用
| 场景 | 说明 |
|---|---|
| 字符串常量池 | 相同字符串字面量复用同一个 String 对象 |
| Integer 缓存 | Integer.valueOf(-128~127) 返回缓存对象 |
| 数据库连接池 / 线程池 | 昂贵资源复用,避免反复创建销毁 |
| 字符/词法分析器 | 大量细粒度 token 对象共享 |
| RPC 连接复用 | 相同目标的连接复用 |
适用场景
- 系统中有大量相似对象,导致内存占用过高;
- 对象大部分状态可以外部化;
- 需要缓冲池来复用昂贵资源。
小结
- 享元模式通过共享技术支持大量细粒度对象,减少对象数量,降低内存。
- 核心是内外部状态分离:内部状态可共享进缓存,外部状态由客户端传入。
- 实现:Map 缓存 + 复用,取用返回已有对象,没有才创建。
- 两大难点:线程安全、内外部状态划分(会复杂化系统逻辑)。
- 一句话:颜色共享、位置外传——把不变的东西缓存,把变化的东西传参。