设计模式手册011:享元模式 - 共享细粒度对象的高效之道
本文是「设计模式手册」系列第011篇,我们将深入探讨享元模式,这种模式通过共享技术来有效地支持大量细粒度的对象,是性能优化的利器。
1. 场景:我们为何需要享元模式?
在软件开发中,我们有时需要创建大量相似的对象,这些对象的部分内部状态可以共享,如果直接创建大量对象会消耗大量内存。例如:
- 文本编辑器中的字符对象:每个字符的字体、大小、颜色等可以共享,而位置不同
- 游戏中的子弹对象:子弹的图片、伤害值等可以共享,而位置、方向不同
- 数据库连接池:连接参数相同,但连接状态不同
- 线程池:线程的配置相同,但执行的任务不同
传统做法的困境:
java
表示游戏中的树木,每个树木对象都包含所有状态
public class Tree {
private String type; 树木类型(内在状态,可共享)
private int x; x坐标(外在状态,不可共享)
private int y; y坐标(外在状态,不可共享)
private int age; 树龄(外在状态,不可共享)
public Tree(String type, int x, int y, int age) {
this.type = type;
this.x = x;
this.y = y;
this.age = age;
}
public void draw() {
System.out.println(在( + x + , + y + )绘制一棵 + type + 树木,树龄 + age);
}
}
游戏场景中需要渲染大量树木
public class Game {
public static void main(String[] args) {
ListTree trees = new ArrayList();
创建1000棵橡树,每棵位置和树龄不同
for (int i = 0; i 1000; i++) {
trees.add(new Tree(橡树, i % 100, i 10, i % 10));
}
渲染所有树木
for (Tree tree trees) {
tree.draw();
}
问题:1000棵树需要创建1000个对象,每个对象都存储了类型字符串,造成内存浪费
}
}
这种实现的痛点:
- 内存浪费:大量对象存储重复的内在状态
- 创建开销:大量相似对象的创建和销毁消耗资源
- 性能下降:内存占用高导致GC频繁,影响性能
2. 享元模式:定义与本质
2.1 模式定义
享元模式(Flyweight Pattern):运用共享技术有效地支持大量细粒度的对象。
2.2 核心角色
java
享元接口:定义享元对象的接口,通过这个接口可以传入外部状态
public interface TreeFlyweight {
void draw(int x, int y, int age);
}
具体享元类:实现享元接口,并为内部状态增加存储空间
public class TreeType implements TreeFlyweight {
private String type; 内部状态(可共享)
private String color; 内部状态(可共享)
private String texture; 内部状态(可共享)
public TreeType(String type, String color, String texture) {
this.type = type;
this.color = color;
this.texture = texture;
}
@Override
public void draw(int x, int y, int age) {
System.out.println(在( + x + , + y + )绘制一棵 + type +
树木,颜色 + color + ,纹理 + texture + ,树龄 + age);
}
}
享元工厂:创建并管理享元对象
public class TreeFactory {
private static MapString, TreeType treeTypes = new HashMap();
public static TreeType getTreeType(String type, String color, String texture) {
String key = type + _ + color + _ + texture;
TreeType treeType = treeTypes.get(key);
if (treeType == null) {
treeType = new TreeType(type, color, texture);
treeTypes.put(key, treeType);
System.out.println(创建新的树木类型 + key);
} else {
System.out.println(重用已有的树木类型 + key);
}
return treeType;
}
public static int getTreeTypeCount() {
return treeTypes.size();
}
}
上下文:包含外部状态和享元对象的引用
public class Tree {
private int x; 外部状态
private int y; 外部状态
private int age; 外部状态
private TreeType treeType; 享元对象(内部状态)
public Tree(int x, int y, int age, TreeType treeType) {
this.x = x;
this.y = y;
this.age = age;
this.treeType = treeType;
}
public void draw() {
treeType.draw(x, y, age);
}
}
3. 深入理解:享元模式的多维视角
3.1 第一重:内部状态 vs 外部状态
核心概念:区分内部状态(可共享)和外部状态(不可共享)
java
内部状态(Intrinsic State):存储在享元对象内部,不会随环境改变而改变,可以共享
外部状态(Extrinsic State):随环境改变而改变,不能共享,由客户端保存
public class Game {
public static void main(String[] args) {
ListTree trees = new ArrayList();
先获取享元对象(内部状态)
TreeType oakType = TreeFactory.getTreeType(橡树, 绿色, 粗糙);
TreeType pineType = TreeFactory.getTreeType(松树, 深绿, 光滑);
创建大量树木,每个树木有自己的外部状态
for (int i = 0; i 1000; i++) {
TreeType type = (i % 2 == 0) oakType pineType;
trees.add(new Tree(i % 100, i 10, i % 10, type));
}
渲染所有树木
for (Tree tree trees) {
tree.draw();
}
System.out.println(总共创建的树木类型数量 + TreeFactory.getTreeTypeCount());
输出:总共创建的树木类型数量 2
我们创建了1000棵树,但只创建了2个树木类型对象,大大节省了内存
}
}
3.2 第二重:享元池的管理
java
带缓存的享元工厂
public class TreeFactory {
private static MapString, TreeType treeTypes = new HashMap();
private static final int MAX_SIZE = 100; 防止无限增长
public static TreeType getTreeType(String type, String color, String texture) {
String key = type + _ + color + _ + texture;
尝试从缓存获取
TreeType treeType = treeTypes.get(key);
if (treeType == null) {
如果缓存已满,移除最早的一个(简单策略)
if (treeTypes.size() = MAX_SIZE) {
String firstKey = treeTypes.keySet().iterator().next();
treeTypes.remove(firstKey);
System.out.println(移除树木类型 + firstKey);
}
treeType = new TreeType(type, color, texture);
treeTypes.put(key, treeType);
System.out.println(创建新的树木类型 + key);
}
return treeType;
}
public static void clearCache() {
treeTypes.clear();
}
public static int getCacheSize() {
return treeTypes.size();
}
}
3.3 第三重:线程安全的享元模式
java
线程安全的享元工厂
public class ThreadSafeTreeFactory {
private static MapString, TreeType treeTypes = new ConcurrentHashMap();
public static TreeType getTreeType(String type, String color, String texture) {
String key = type + _ + color + _ + texture;
使用computeIfAbsent保证原子性
return treeTypes.computeIfAbsent(key, k - {
System.out.println(创建新的树木类型 + k);
return new TreeType(type, color, texture);
});
}
使用读锁和写锁进一步控制并发
private static final ReadWriteLock lock = new ReentrantReadWriteLock();
public static TreeType getTreeTypeWithLock(String type, String color, String texture) {
String key = type + _ + color + _ + texture;
lock.readLock().lock();
try {
TreeType existing = treeTypes.get(key);
if (existing != null) {
return existing;
}
} finally {
lock.readLock().unlock();
}
lock.writeLock().lock();
try {
双重检查
TreeType existing = treeTypes.get(key);
if (existing == null) {
existing = new TreeType(type, color, texture);
treeTypes.put(key, existing);
System.out.println(创建新的树木类型 + key);
}
return existing;
} finally {
lock.writeLock().unlock();
}
}
}
4. 实战案例:完整的文本编辑器
java
字符享元
public interface CharacterFlyweight {
void draw(int x, int y, String color); 颜色作为外部状态
String getFont();
int getSize();
}
具体字符享元
public class CharacterType implements CharacterFlyweight {
private char character;
private String font;
private int size;
public CharacterType(char character, String font, int size) {
this.character = character;
this.font = font;
this.size = size;
}
@Override
public void draw(int x, int y, String color) {
System.out.println(在( + x + , + y + )绘制字符' + character +
',字体 + font + ,大小 + size + ,颜色 + color);
}
@Override
public String getFont() {
return font;
}
@Override
public int getSize() {
return size;
}
public char getCharacter() {
return character;
}
}
字符工厂
public class CharacterFactory {
private static MapString, CharacterType characters = new HashMap();
public static CharacterType getCharacter(char c, String font, int size) {
String key = c + _ + font + _ + size;
return characters.computeIfAbsent(key, k - new CharacterType(c, font, size));
}
public static int getCharacterCount() {
return characters.size();
}
}
文档中的字符(包含外部状态)
public class DocumentCharacter {
private int x;
private int y;
private String color;
private CharacterType characterType;
public DocumentCharacter(int x, int y, String color, CharacterType characterType) {
this.x = x;
this.y = y;
this.color = color;
this.characterType = characterType;
}
public void draw() {
characterType.draw(x, y, color);
}
}
文档类
public class Document {
private ListDocumentCharacter characters = new ArrayList();
public void addCharacter(char c, String font, int size, String color, int x, int y) {
CharacterType charType = CharacterFactory.getCharacter(c, font, size);
characters.add(new DocumentCharacter(x, y, color, charType));
}
public void render() {
for (DocumentCharacter docChar characters) {
docChar.draw();
}
}
public int getCharacterCount() {
return characters.size();
}
}
使用示例
public class TextEditor {
public static void main(String[] args) {
Document doc = new Document();
添加大量字符
String text = Hello, Flyweight Pattern!;
String[] fonts = {Arial, Times New Roman, Courier};
String[] colors = {黑色, 红色, 蓝色};
Random random = new Random();
for (int i = 0; i text.length(); i++) {
char c = text.charAt(i);
String font = fonts[random.nextInt(fonts.length)];
int size = 12 + random.nextInt(6); 12-17号字
String color = colors[random.nextInt(colors.length)];
int x = i 10;
int y = 0;
doc.addCharacter(c, font, size, color, x, y);
}
渲染文档
doc.render();
System.out.println(文档字符数 + doc.getCharacterCount());
System.out.println(创建的字符类型数 + CharacterFactory.getCharacterCount());
字符类型数远小于文档字符数,因为重复的字符、字体、大小组合被共享了
}
}
5. Java标准库中的享元模式
5.1 Integer的享元应用
java
Java Integer类的享元模式
public class IntegerFlyweightExample {
public static void main(String[] args) {
Integer缓存了-128到127的值
Integer a = Integer.valueOf(127);
Integer b = Integer.valueOf(127);
System.out.println(a == b + (a == b)); true,同一个对象
Integer c = Integer.valueOf(128);
Integer d = Integer.valueOf(128);
System.out.println(c == d + (c == d)); false,不同对象
字符串常量池也是享元模式的应用
String s1 = hello;
String s2 = hello;
System.out.println(s1 == s2 + (s1 == s2)); true
}
}
5.2 连接池中的享元思想
java
数据库连接池 - 享元模式的思想
public class ConnectionPool {
private static final int MAX_POOL_SIZE = 10;
private static ListConnection availableConnections = new ArrayList();
private static ListConnection usedConnections = new ArrayList();
static {
initializePool();
}
private static void initializePool() {
for (int i = 0; i MAX_POOL_SIZE; i++) {
availableConnections.add(createConnection());
}
}
private static Connection createConnection() {
创建数据库连接
System.out.println(创建新的数据库连接);
return null; 实际返回Connection对象
}
public static Connection getConnection() {
if (availableConnections.isEmpty()) {
System.out.println(没有可用连接,等待...);
return null;
}
Connection connection = availableConnections.remove(availableConnections.size() - 1);
usedConnections.add(connection);
return connection;
}
public static void releaseConnection(Connection connection) {
usedConnections.remove(connection);
availableConnections.add(connection);
}
public static int getAvailableCount() {
return availableConnections.size();
}
public static int getUsedCount() {
return usedConnections.size();
}
}
6. 享元模式的进阶用法
6.1 复合享元模式
java
复合享元:由多个享元对象组合而成
public class CompositeTreeType implements TreeFlyweight {
private ListTreeType treeTypes = new ArrayList();
public void add(TreeType treeType) {
treeTypes.add(treeType);
}
@Override
public void draw(int x, int y, int age) {
for (TreeType treeType treeTypes) {
treeType.draw(x, y, age);
}
}
}
复合享元工厂
public class CompositeTreeFactory {
private static MapString, CompositeTreeType composites = new HashMap();
public static CompositeTreeType getCompositeType(String compositeKey, ListString treeKeys) {
return composites.computeIfAbsent(compositeKey, k - {
CompositeTreeType composite = new CompositeTreeType();
for (String treeKey treeKeys) {
解析treeKey,获取type, color, texture
String[] parts = treeKey.split(_);
TreeType treeType = TreeFactory.getTreeType(parts[0], parts[1], parts[2]);
composite.add(treeType);
}
return composite;
});
}
}
6.2 享元模式与原型模式结合
java
可克隆的享元对象
public class CloneableTreeType extends TreeType implements Cloneable {
public CloneableTreeType(String type, String color, String texture) {
super(type, color, texture);
}
@Override
public CloneableTreeType clone() {
try {
return (CloneableTreeType) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(克隆失败, e);
}
}
}
带克隆功能的享元工厂
public class CloneableTreeFactory {
private static MapString, CloneableTreeType prototypes = new HashMap();
public static void registerPrototype(String key, CloneableTreeType prototype) {
prototypes.put(key, prototype);
}
public static CloneableTreeType getTreeType(String key) {
CloneableTreeType prototype = prototypes.get(key);
if (prototype != null) {
return prototype.clone();
}
return null;
}
}
7. 享元模式 vs 其他模式
7.1 享元模式 vs 单例模式
- 享元模式:可以有多个享元实例,按需创建和共享
- 单例模式:确保一个类只有一个实例
7.2 享元模式 vs 原型模式
- 享元模式:通过共享减少对象数量
- 原型模式:通过克隆创建新对象
7.3 享元模式 vs 对象池模式
- 享元模式:关注共享不可变的内在状态
- 对象池模式:关注重用昂贵的对象(如数据库连接)
8. 总结与思考
8.1 享元模式的优点
- 大幅减少内存使用:通过共享相似对象减少内存占用
- 提高性能:减少对象创建和垃圾回收的开销
- 统一管理:享元工厂可以统一管理和控制享元对象
- 扩展性好:可以方便地增加新的享元类型
8.2 享元模式的缺点
- 增加系统复杂性:需要分离内部状态和外部状态
- 线程安全问题:需要确保享元工厂的线程安全
- 可能引入同步开销:享元工厂的同步可能影响性能
- 使用场景有限:只有在存在大量相似对象时才有效
8.3 设计思考
享元模式的本质是状态分离与共享。它通过将对象的状态分为内部状态(可共享)和外部状态(不可共享),使得大量细粒度对象可以共享相同的内部状态,从而显著减少内存使用。
深入思考的角度:
享元模式的核心价值在于它通过共享技术解决了大量相似对象的内存消耗问题。它让我们能够在有限的资源下支持更多的对象,是空间换时间的经典体现。
在实际应用中,享元模式有很多优秀的实践:
- Java字符串常量池
- Integer等包装类的缓存
- 各种连接池技术
- 游戏开发中的精灵对象
从系统设计的角度看,享元模式特别适合以下场景:
- 系统需要创建大量相似对象
- 对象的大部分状态可以外部化
- 使用享元模式后能显著减少内存占用
- 对象身份不是特别重要(可以共享)
最佳实践建议:
- 仔细分析对象的内部状态和外部状态
- 确保享元对象的不可变性
- 使用合适的缓存策略管理享元对象
- 考虑线程安全性
- 不要过度使用,只有在确实需要时才使用
使用场景判断:
- 适合:大量相似对象、内存敏感的场景、对象状态可分离
- 不适合:对象间差异很大、外部状态复杂、性能要求不高的场景
下一篇预告:设计模式手册012 - 责任链模式:如何让多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系?
版权声明:本文为CSDN博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。