开篇:你的系统是否遭遇这些痛点?
在互联网高并发场景下,你是否遇到过这样的问题:
创建大量类似对象导致内存暴涨?
频繁创建销毁对象引发GC频繁,系统卡顿?
数据库连接、线程资源消耗巨大?
今天,我们将深入探讨一个经典而强劲的设计模式——享元模式(Flyweight Pattern),看它如何在JDK源码、各大开源框架中大显身手,协助我们优雅地解决这些难题!
一、什么是享元模式?
通过共享技术有效支持大量细粒度对象的复用。
享元模式将对象的状态分为两类:
内部状态(Intrinsic State)
存储在享元对象内部
不会随环境改变而改变
可以共享
外部状态(Extrinsic State)
随环境改变而改变
不可共享
由客户端保存并传入
适用场景
✅ 系统中存在大量类似对象
✅ 对象的大部分状态可以外部化
✅ 需要缓冲池的场景(连接池、线程池)
✅ 使用享元模式的额外开销能被节省的内存抵消
模式结构
享元模式包含四大角色:
Flyweight(抽象享元)
定义享元对象的接口
ConcreteFlyweight(具体享元)
实现抽象享元接口
UnsharedConcreteFlyweight(非共享享元)
不需要共享的享元子类
FlyweightFactory(享元工厂)
负责创建和管理享元对象

享元模式系统架构
二、经典案例:五子棋游戏中的棋子管理
问题场景
想象一个五子棋游戏,棋盘有15×15=225个位置。如果为每个棋子都创建一个对象,内存消耗巨大。但实际上:
- 所有黑棋的颜色、形状都一样,只有位置不同
- 所有白棋的颜色、形状都一样,只有位置不同
关键洞察: 颜色、形状是内部状态(可共享),位置是外部状态(不可共享)
代码实现
/ 抽象享元:棋子接口
public interface ChessPiece {
void display(int x, int y);
}
// 具体享元:具体棋子
public class ConcreteChessPiece implements ChessPiece {
private String color; // 内部状态:颜色
private String shape; // 内部状态:形状
public ConcreteChessPiece(String color) {
this.color = color;
this.shape = "圆形";
System.out.println("创建了一个" + color + "棋子对象");
}
@Override
public void display(int x, int y) {
System.out.println("在位置[" + x + "," + y + "]放置" + color + shape + "棋子");
}
}
// 享元工厂
public class ChessPieceFactory {
private static final Map<String, ChessPiece> pool = new HashMap<>();
public static ChessPiece getChessPiece(String color) {
ChessPiece piece = pool.get(color);
if (piece == null) {
piece = new ConcreteChessPiece(color);
pool.put(color, piece);
}
return piece;
}
public static int getTotalPieces() {
return pool.size();
}
}
运行结果
创建了一个黑色棋子对象
在位置[1,1]放置黑色圆形棋子
创建了一个白色棋子对象
在位置[1,2]放置白色圆形棋子
在位置[2,1]放置黑色圆形棋子
在位置[2,2]放置白色圆形棋子
实际创建的棋子对象数量:2
black1 == black2: true
效果: 放置了4个棋子,实际只创建了2个对象!内存节省50%!

五子棋游戏
三、JDK中的享元模式应用
3.1 String常量池
Java的String常量池是享元模式的典型应用!
public class StringPoolExample {
public static void main(String[] args) {
String s1 = "Hello"; // 字符串字面量,存储在常量池
String s2 = "Hello"; // 复用常量池中的对象
String s3 = "Hello"; // 复用常量池中的对象
String s4 = new String("Hello"); // 新对象,不在常量池
String s5 = s4.intern(); // 手动加入常量池
System.out.println("s1 == s2: " + (s1 == s2)); // true
System.out.println("s1 == s3: " + (s1 == s3)); // true
System.out.println("s1 == s4: " + (s1 == s4)); // false
System.out.println("s1 == s5: " + (s1 == s5)); // true
}
}
原理: JVM自动将字符串字面量放入常量池,实现自动共享!
3.2 Integer缓存池
Java对基本类型包装类也应用了享元模式。Integer类对-128到127之间的整数进行了缓存。
public class IntegerCacheExample {
public static void main(String[] args) {
// 在缓存范围内(-128到127)
Integer a1 = 100;
Integer a2 = 100;
System.out.println("a1 == a2: " + (a1 == a2)); // true
// 超出缓存范围
Integer b1 = 200;
Integer b2 = 200;
System.out.println("b1 == b2: " + (b1 == b2)); // false
}
}
源码揭秘:Integer.valueOf()
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

JDK中的享元模式
四、生产级应用:数据库连接池
为什么需要连接池?
数据库连接是一种重量级资源,创建和销毁连接的开销超级大:
- TCP三次握手建立连接:耗时10-50ms
- 数据库认证过程:耗时5-20ms
- 资源分配(内存、文件描述符等)
⚠️ 痛点: 如果每次数据库操作都创建新连接,在高并发场景下系统性能将严重下降!
简化版连接池实现
/ 数据库连接(享元对象)
public class DatabaseConnection {
private String connectionId;
private boolean inUse;
public DatabaseConnection(String id) {
this.connectionId = id;
this.inUse = false;
// 模拟创建连接的耗时操作
try {
Thread.sleep(100);
System.out.println("创建数据库连接:" + id);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void executeQuery(String sql) {
System.out.println("[" + connectionId + "] 执行SQL: " + sql);
}
// getter和setter省略...
}
// 连接池工厂(享元工厂)
public class ConnectionPool {
private static final int POOL_SIZE = 5;
private List<DatabaseConnection> connections = new ArrayList<>();
public ConnectionPool() {
// 初始化连接池
for (int i = 0; i < POOL_SIZE; i++) {
connections.add(new DatabaseConnection("CONN-" + (i + 1)));
}
}
public synchronized DatabaseConnection getConnection() {
for (DatabaseConnection conn : connections) {
if (!conn.isInUse()) {
conn.setInUse(true);
System.out.println("从连接池获取连接:" + conn.getConnectionId());
return conn;
}
}
System.out.println("连接池已满,等待中...");
return null;
}
public synchronized void releaseConnection(DatabaseConnection conn) {
conn.setInUse(false);
System.out.println("释放连接回连接池:" + conn.getConnectionId());
}
}
性能对比:震撼的数据
|
指标 |
不使用连接池 |
使用连接池 |
性能提升 |
|
每次操作耗时 |
~150ms |
~5ms |
30倍 ⚡ |
|
1000次操作总耗时 |
~150秒 |
~5秒 |
30倍 ⚡ |
|
内存占用 |
不稳定(频繁GC) |
稳定 |
减少70% |
|
并发能力 |
低 |
高 |
提升80% |

数据库连接池性能对比
️ 五、开源框架中的享元模式
5.1 Apache Commons Pool
Apache Commons Pool是一个通用的对象池化框架,广泛应用于各种池化场景。
import org.apache.commons.pool2.impl.GenericObjectPool;
// 配置对象池
GenericObjectPoolConfig<ExpensiveObject> config = new GenericObjectPoolConfig<>();
config.setMaxTotal(5); // 最大对象数
config.setMaxIdle(3); // 最大空闲对象数
config.setMinIdle(1); // 最小空闲对象数
// 创建对象池
GenericObjectPool<ExpensiveObject> pool =
new GenericObjectPool<>(new ExpensiveObjectFactory(), config);
// 使用对象池
ExpensiveObject obj = pool.borrowObject();
obj.doWork("任务-1");
pool.returnObject(obj);
5.2 线程池(ThreadPoolExecutor)
Java的线程池也是享元模式的典型应用,通过复用线程避免频繁创建销毁的开销。
// 创建固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(3);
// 提交10个任务,只会创建3个线程复用
for (int i = 0; i < 10; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("任务 " + taskId +
" 由线程 " + Thread.currentThread().getName() + " 执行");
});
}

线程池工作量原理
六、享元模式的优缺点
✅ 优点
1.大幅减少对象创建数量
,降低内存占用
2.提高系统性能
,避免频繁GC
3.提高对象复用率
,减少创建销毁开销
4.外部状态独立
,不会影响内部状态
❌ 缺点
1.增加系统复杂度,需要分离内部和外部状态
2.读取外部状态的开销,可能抵消部分性能提升
3.线程安全问题,共享对象需要思考并发访问
4.不适合状态常常变化的对象
七、最佳实践与注意事项
何时使用享元模式?
✔️ 必须满足:
- 对象数量巨大(成百上千)
- 内存压力大(频繁GC)
- 对象可共享(大部分状态可外部化)
- 创建开销大(耗时或消耗资源多)
实现要点
public class FlyweightBestPractice {
// 1️⃣ 使用线程安全的容器
private static final ConcurrentHashMap<String, Object> pool =
new ConcurrentHashMap<>();
// 2️⃣ 使用双重检查锁确保线程安全
public static Object getFlyweight(String key) {
Object obj = pool.get(key);
if (obj == null) {
synchronized (pool) {
obj = pool.get(key);
if (obj == null) {
obj = createObject(key);
pool.put(key, obj);
}
}
}
return obj;
}
// 3️⃣ 设置池的大小上限,避免内存溢出
private static final int MAX_POOL_SIZE = 100;
// 4️⃣ 提供清理机制
public static void clear() {
pool.clear();
}
}
与其他模式的协作
与工厂模式结合
:享元工厂负责创建和管理享元对象
与单例模式结合
:享元工厂一般设计为单例
与状态模式结合
:享元对象的状态变化可以用状态模式管理
与组合模式结合
:可以将享元对象组合成更复杂的结构

和其他模式对比
八、总结
享元模式是一个强劲的性能优化工具,通过对象共享实现内存和性能的双重优化。在实际开发中,我们已经在使用它:
JDK自带:
String常量池
包装类缓存池(Integer、Long等)
数据库领域:
连接池(HikariCP、Druid)
并发编程:
线程池(ThreadPoolExecutor)
缓存框架:
Redis连接池、对象池
关键要点回顾
✨ 区分内部状态和外部状态
✨ 使用工厂管理享元对象
✨ 注意线程安全问题
✨ 在合适的场景使用,避免过度设计
收藏了,感谢分享