深入浅出设计模式【十一、享元模式】
一、享元模式介绍
在软件系统中,有时需要创建大量细粒度的对象(例如,文档中的每一个字符、棋盘上的每一个棋子、地图上的每一棵树)。如果这些对象除了少数几个参数外其余都相同,那么大量创建它们会导致高昂的内存开销和性能损耗。
享元模式通过区分对象的“内部状态”和“外部状态”来解决这个问题。内部状态是对象中可以共享的、不变的部分,它独立于对象的场景。外部状态是对象中随场景变化而变化的部分,不能共享。享元模式将内部状态分离出来进行共享,而外部状态由客户端在使用时提供。这样,只需要一个共享对象(享元)就可以代表所有具有相同内部状态的对象,极大地减少了所需对象的数量。
二、核心概念与意图
-
核心概念:
- 享元 (Flyweight): 描述一个接口,通过这个接口享元可以接受并作用于外部状态。
- 具体享元 (Concrete Flyweight): 实现享元接口,并为内部状态增加存储空间。它必须是可共享的对象,其内部状态必须与场景无关。
- 非共享具体享元 (Unshared Concrete Flyweight): 并非所有的享元子类都需要被共享。不能共享的享元通常将外部状态作为内部状态存储(但这并非强制)。
- 享元工厂 (Flyweight Factory): 创建并管理享元对象。它确保合理地共享享元:当用户请求一个享元时,工厂提供一个已创建的实例(如果存在),或者创建一个新的实例(如果不存在)。
- 客户端 (Client): 维持一个对享元的引用;计算或存储一个或多个外部状态。
-
意图:
- 运用共享技术有效地支持大量细粒度的对象。
- 通过共享相同的内在状态,避免大量非常相似类的开销。
- 减少内存占用,提升性能。
三、适用场景剖析
享元模式在以下场景中非常有效:
- 一个应用程序使用了大量的对象: 这会导致很大的存储开销。
- 对象的大多数状态都可以变为外部状态: 即对象的大部分状态可以从其场景中分离出来。
- 剥离外部状态后,可以用相对较少的共享对象取代大量对象。
- 应用程序不依赖于对象标识: 由于享元对象可以被共享,因此从概念上讲,它不再是同一个对象。客户端不能对享元对象做身份测试(如
==比较),这通常不会成为问题,因为享元对象通常封装的是原始数据(如字符、数字)。
典型例子:
- 文本编辑器: 每个字符都是一个对象,其内部状态是字符代码(如 ‘a’),外部状态是它的位置(行、列)和格式(字体、颜色)。成千上万个 ‘a’ 字符共享同一个享元对象。
- 图形应用: 大量相同的树、石头、士兵等。内部状态是纹理、模型,外部状态是位置、大小。
- 游戏开发: 大量相同或相似的游戏实体。
- 数据库连接池、线程池: 在某种意义上,池化技术是享元模式的一种变体和应用。
四、UML 类图解析(Mermaid)
以下UML类图清晰地展示了享元模式的结构和角色间的关系:
Flyweight(享元接口): 声明一个接口,享元对象通过该接口可以接受并作用于外部状态 (extrinsicState)。ConcreteFlyweight(具体享元):- 实现享元接口。
- 包含内部状态 (
intrinsicState),该状态必须是可共享的,且独立于其场景。 - 其
operation(extrinsicState)方法的实现必须使用传入的外部状态来执行逻辑。
UnsharedConcreteFlyweight(非共享具体享元): 并非所有的享元子类都需要被共享。它可能直接存储所有状态,但仍然通过统一的接口与客户端交互。FlyweightFactory(享元工厂):- 核心管理角色。通常维护一个对象池 (
pool: Map<String, Flyweight>)。 - 键 (
key) 通常代表内部状态。 - 当客户端请求一个享元时,工厂检查池中是否存在具有相同内部状态的享元。如果存在,则返回它;如果不存在,则创建一个新的享元,将其放入池中,然后返回它。
- 核心管理角色。通常维护一个对象池 (
Client(客户端):- 负责计算或存储外部状态 (
extrinsicState)。 - 从
FlyweightFactory获取Flyweight对象。 - 在调用享元对象的操作时,将外部状态作为参数传入。
- 负责计算或存储外部状态 (
五、各种实现方式及其优缺点
享元模式的实现关键在于如何管理内部状态和外部状态。
1. 标准实现(接口+工厂+对象池)
即上述UML所描述的方式,这是最经典和推荐的方式。
// 1. Flyweight Interface
public interface Shape {
void draw(int x, int y, String color); // extrinsicState: x, y, color
}
// 2. Concrete Flyweight (Intrinsic State: shapeType)
public class Circle implements Shape {
private String type; // Intrinsic State (shared)
public Circle(String type) {
this.type = type;
}
@Override
public void draw(int x, int y, String color) { // extrinsicState passed in
System.out.println("Drawing a " + type + " circle at (" + x + ", " + y + ") with color " + color);
}
}
// 3. Flyweight Factory
import java.util.HashMap;
import java.util.Map;
public class ShapeFactory {
private static final Map<String, Shape> circleMap = new HashMap<>();
public static Shape getCircle(String type) {
Shape circle = circleMap.get(type);
if (circle == null) {
// Create a new one and put it in the pool
circle = new Circle(type);
circleMap.put(type, circle);
System.out.println("Creating circle of type: " + type);
}
return circle;
}
}
// 4. Client
public class Client {
private static final String[] colors = {"Red", "Green", "Blue", "White", "Black"};
private static final String[] types = {"Small", "Medium", "Large"};
public static void main(String[] args) {
for (int i = 0; i < 20; ++i) {
// Randomly get a type (intrinsic state key)
String type = getRandomType();
// Get the flyweight from Factory
Shape circle = ShapeFactory.getCircle(type);
// Use the flyweight, passing extrinsic state
circle.draw(getRandomX(), getRandomY(), getRandomColor());
}
}
// ... helper methods getRandomType, getRandomX, getRandomY, getRandomColor
}
- 优点:
- 极大减少内存消耗: 内存中仅存储不同内部状态的对象各一份。
- 可能提升性能: 减少了对象创建和垃圾回收的开销。
- 缺点:
- 增加系统复杂性: 需要分离内部状态和外部状态,这使得逻辑变得更加复杂。
- 可能引入线程安全问题: 享元工厂的池通常是全局的,在多线程环境下需要保证其线程安全。上面的简单实现不是线程安全的。
- 牺牲查询效率来换取空间: 工厂需要管理一个池,并执行查询操作。
2. 内部状态不可变
这是享元模式的一个关键约束。共享的享元对象其内部状态必须是不可变的。否则,一个客户端的修改会影响所有共享该享元的客户端,导致难以预料的错误。
- 实现: 将享元对象的内部状态字段声明为
final,并通过构造函数初始化,不提供setter方法。
六、最佳实践
- 确保内部状态不可变: 这是享元模式正确运行的基石。内部状态必须在对象创建时初始化,并且在对象的生命周期内永不改变。
- 使用工厂模式管理享元: 享元的创建和获取必须通过一个集中的工厂来控制,以确保共享的正确性。
- 谨慎处理线程安全: 享元工厂中的对象池通常是共享资源。在多线程应用中,必须使用同步机制(如
ConcurrentHashMap)来保证线程安全。 - 与对象池模式区分:
- 享元模式: 目的是减少内存占用,通过共享对象的不可变部分(内部状态)来实现。对象一旦创建,通常不会销毁。
- 对象池模式: 目的是减少对象创建和销毁的开销(如数据库连接、线程)。池中的对象是完整的、可变的,客户从池中借出对象,使用完毕后归还,这些对象的状态会被重置。
- 仅在必要时使用: 享元模式引入了额外的复杂性。只有在确实存在大量相似对象并导致性能问题时才应考虑使用。不要过早优化。
七、在开发中的演变和应用
享元模式的思想在现代开发中演变为更广泛的概念和应用:
- 缓存 (Caching): 享元模式本质上是一种对象级别的缓存策略。它将创建成本高或内存占用大的对象缓存起来复用。现代缓存框架(如Ehcache, Redis)可以看作是分布式、通用化的享元模式实现。
- 池化技术 (Pooling): 数据库连接池、线程池是享元思想的延伸。它们共享和复用的是昂贵的资源(连接、线程),而不是简单的对象内部状态。
- 内容交付网络 (CDN): 在架构层面,CDN通过在全球边缘节点缓存(共享)静态资源(图片、视频),使用户可以从最近的位置获取,这可以看作是一种宏观的享元模式,外部状态是用户的地理位置。
- 驻留 (Intern): 字符串驻留是享元模式最直接的应用。
八、真实开发案例(Java语言内部、知名开源框架、工具)
-
Java String Pool (字符串常量池):
- 这是享元模式在Java语言中最经典、最成功的应用。
- 内部状态: 字符串的字面值(如
"hello")。 - 享元工厂: JVM中的字符串常量池。
- 行为: 当使用双引号(
String s = "hello")创建字符串时,JVM会首先在池中查找是否存在相同值的字符串。如果找到,则返回该引用;如果未找到,则将其放入池中并返回新引用。 - 这解释了为什么
String s1 = "hello"; String s2 = "hello";中s1 == s2为true。
-
Java Integer Cache:
- Java对
Integer对象也进行了缓存(享元)。
Integer i1 = 127; // Auto-boxing, gets cached instance Integer i2 = 127; System.out.println(i1 == i2); // true Integer i3 = 128; Integer i4 = 128; System.out.println(i3 == i4); // false (outside default cache range of -128 to 127)- JVM默认缓存了
-128到127之间的Integer对象。通过valueOf(int)方法(自动装箱调用)会返回缓存的对象。
- Java对
-
Apache Commons Pool 2:
- 虽然它更偏向于对象池模式,但其核心思想与享元模式一脉相承——通过复用对象来提升性能。它提供了一个强大的通用对象池化框架,用于管理如数据库连接、Socket连接等昂贵对象的生命周期。
-
GUI 工具包中的字型 (Font) 和图标 (Icon) 对象:
- 在Swing等GUI框架中,相同的字体或图标(内部状态)在应用程序中通常只存在一份。控件(如按钮、标签)在绘制时,将自己的位置和文本(外部状态)传递给字体和图标对象进行渲染。
-
日志框架:
- 每个
Logger实例通常按名称(如类名)获取。对同一个名称的Logger的获取总是返回同一个实例。这可以看作是一种享元模式,Logger的名称是内部状态。
- 每个
九、总结
| 方面 | 总结 |
|---|---|
| 模式类型 | 结构型设计模式 |
| 核心意图 | 运用共享技术有效地支持大量细粒度的对象,减少内存占用。 |
| 关键角色 | 享元(Flyweight), 具体享元(ConcreteFlyweight), 享元工厂(FlyweightFactory), 客户端(Client) |
| 核心概念 | 分离状态: 内部状态 (intrinsic, 共享, 不可变) vs. 外部状态 (extrinsic, 非共享, 由客户端传入)。 |
| 主要优点 | 1. 极大减少内存中对象的数量。 2. 可能提升性能(减少GC,减少创建开销)。 |
| 主要缺点 | 1. 增加系统复杂性: 需要分离内外状态,逻辑变复杂。 2. 牺牲时间换空间: 工厂查询需要时间。 3. 线程安全问题: 工厂需要线程安全。 |
| 适用场景 | 系统中有大量相似对象,且1) 大部分状态可外部化,2) 删除外部状态后可用较少的共享对象替代。 |
| 最佳实践 | 内部状态必须不可变;使用工厂集中管理;注意线程安全;与对象池模式区分。 |
| 关系与对比 | vs. 对象池: 享元共享不可变内部状态;对象池共享完整的可变对象(借与还)。 vs. 单例: 享元是“多例”的,工厂根据不同的内部状态返回不同的实例;单例是“单例”的,全局只有一个实例。 |
| 现代应用 | 缓存技术、池化技术的理论基础之一。 |
| 真实案例 | Java String Pool (典范),Integer Cache,Apache Commons Pool,GUI 资源管理。 |
享元模式是一种“以时间换空间”的经典模式,是优化性能、减少资源消耗的强大工具。它在处理大量重复对象的场景下效果显著。然而,其引入的复杂性也要求开发者在应用时必须仔细权衡,确保真正符合使用场景。理解并正确应用享元模式,是处理高性能、高并发、低内存占用系统设计的关键技能之一。
本文来自博客园,作者:NeoLshu,转载请注明原文链接:https://www.cnblogs.com/neolshu/p/19120901

浙公网安备 33010602011771号