一、享元模式概述
享元模式(Flyweight Pattern)
定义
享元模式是一种结构型设计模式,旨在通过共享对象来最小化内存使用或计算开销。其核心思想是将对象的“不变部分”(内部状态)与“可变部分”(外部状态)分离,通过共享不变部分来减少重复对象的创建。
核心思想
-
共享对象
将多个对象共用的数据(内部状态)提取出来,存储在共享的“享元对象”中,避免重复存储相同数据。 -
分离状态
- 内部状态(Intrinsic State):对象中不变的、可共享的部分(例如字符的字体、颜色)。
- 外部状态(Extrinsic State):对象中变化的、不可共享的部分(例如字符在文本中的位置)。
外部状态由客户端在运行时传入,不存储在享元对象中。
-
工厂管理
通过工厂类(Flyweight Factory)集中管理享元对象的创建与复用,确保相同的内部状态仅对应一个享元对象。
类比说明
假设一个文本编辑器需要渲染大量相同的字符(例如字母“A”)。
- 传统方式:为每个“A”创建一个独立对象,存储重复的字体、颜色等数据,浪费内存。
- 享元模式:仅创建一个共享的“A”对象,存储字体和颜色(内部状态),而位置(外部状态)由每次渲染时动态传入。
设计模式分类
设计模式是软件工程中解决常见问题的可复用解决方案。根据其用途和范围,设计模式可以分为三大类:创建型模式、结构型模式和行为型模式。
创建型模式
创建型模式关注对象的创建机制,提供灵活的方式来创建对象,同时隐藏创建逻辑。
-
单例模式(Singleton)
- 确保一个类只有一个实例,并提供全局访问点。
- 适用于需要频繁创建和销毁的对象,如数据库连接池、线程池等。
-
工厂方法模式(Factory Method)
- 定义一个创建对象的接口,但由子类决定实例化哪个类。
- 适用于需要扩展性强、支持多态创建的场景。
-
抽象工厂模式(Abstract Factory)
- 提供一个接口,用于创建相关或依赖对象的家族,而不需要指定具体类。
- 适用于需要创建一组相关对象的场景,如跨平台的UI组件。
-
建造者模式(Builder)
- 将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示。
- 适用于需要分步骤构建复杂对象的场景,如配置对象的构造。
-
原型模式(Prototype)
- 通过复制现有对象来创建新对象,而不是通过实例化类。
- 适用于创建成本较高的对象,如深拷贝复杂对象。
结构型模式
结构型模式关注类和对象的组合方式,形成更大的结构。
-
适配器模式(Adapter)
- 将一个类的接口转换成客户希望的另一个接口。
- 适用于整合不兼容的接口,如旧系统与新系统的对接。
-
桥接模式(Bridge)
- 将抽象部分与实现部分分离,使它们可以独立变化。
- 适用于多维度变化的场景,如不同平台和不同功能的组合。
-
组合模式(Composite)
- 将对象组合成树形结构以表示“部分-整体”的层次结构。
- 适用于需要统一处理单个对象和组合对象的场景,如文件系统。
-
装饰器模式(Decorator)
- 动态地给对象添加额外的职责,而不改变其结构。
- 适用于需要动态扩展功能的场景,如Java I/O流。
-
外观模式(Facade)
- 提供一个统一的接口,简化子系统的使用。
- 适用于复杂系统的简化调用,如框架的入口类。
-
享元模式(Flyweight)
- 通过共享技术高效地支持大量细粒度对象。
- 适用于需要减少内存占用的场景,如字符池、线程池。
-
代理模式(Proxy)
- 为其他对象提供一种代理以控制对这个对象的访问。
- 适用于需要控制访问权限或延迟加载的场景,如远程代理、虚拟代理。
行为型模式
行为型模式关注对象之间的通信和职责分配。
-
责任链模式(Chain of Responsibility)
- 将请求的发送者和接收者解耦,使多个对象都有机会处理请求。
- 适用于多级处理的场景,如审批流程。
-
命令模式(Command)
- 将请求封装为对象,以便参数化客户端。
- 适用于需要支持撤销、重做或日志记录的场景。
-
解释器模式(Interpreter)
- 定义语言的文法,并解释该语言中的句子。
- 适用于需要解析特定语法的场景,如正则表达式。
-
迭代器模式(Iterator)
- 提供一种方法顺序访问聚合对象的元素,而不暴露其底层表示。
- 适用于需要统一遍历不同数据结构的场景,如集合类。
-
中介者模式(Mediator)
- 定义一个中介对象来封装一系列对象之间的交互。
- 适用于减少对象间直接耦合的场景,如聊天室。
-
备忘录模式(Memento)
- 在不破坏封装性的前提下,捕获并保存对象的内部状态。
- 适用于需要撤销或恢复状态的场景,如文本编辑器的撤销功能。
-
观察者模式(Observer)
- 定义对象间的一对多依赖关系,当一个对象状态改变时,所有依赖者都会收到通知。
- 适用于事件驱动的场景,如GUI事件监听。
-
状态模式(State)
- 允许对象在其内部状态改变时改变其行为。
- 适用于对象行为依赖于状态的场景,如订单状态流转。
-
策略模式(Strategy)
- 定义一系列算法,封装每个算法,并使它们可以互相替换。
- 适用于需要动态切换算法的场景,如排序策略。
-
模板方法模式(Template Method)
- 定义一个操作中的算法骨架,将某些步骤延迟到子类中实现。
- 适用于固定流程但部分步骤可变的场景,如框架的钩子方法。
-
访问者模式(Visitor)
- 表示一个作用于某对象结构中的各元素的操作,可以在不改变各元素的类的前提下定义新操作。
- 适用于需要对复杂对象结构进行多种操作的场景,如编译器中的语法树遍历。
适用场景分析
享元模式的核心目标是减少内存消耗,通过共享大量细粒度对象来优化性能。以下是一些典型的适用场景:
1. 大量相似对象存在时
当系统中需要创建大量相似对象,且这些对象的大部分状态可以共享时,享元模式尤为有效。例如:
- 游戏开发:大量相同类型的角色或子弹对象,其纹理、模型等内在状态可以共享。
- 文档编辑器:每个字符对象可能包含字体、大小等共享属性,而位置等则作为外部状态。
2. 对象状态可分离为内部与外部
- 内部状态(可共享):独立于具体场景的固定数据(如字符的字体)。
- 外部状态(不可共享):依赖上下文的可变数据(如字符的位置)。
3. 内存占用是瓶颈
当内存资源紧张,且对象实例化开销较大时(如数据库连接池、线程池等资源管理场景)。
4. 缓存需求
需要缓存某些对象以避免重复创建,例如:
- 数据库连接池:连接对象的配置信息(如URL、用户名)作为内部状态共享。
- 图形渲染:重复使用的图像或纹理资源。
常见误区与注意事项
- 过度共享:并非所有对象都适合共享。若外部状态过多或计算复杂,可能反而降低性能。
- 线程安全:共享对象需确保线程安全,尤其是外部状态的传递(如通过参数或线程局部变量)。
- 权衡维护性:代码可能因分离内外状态而更难理解,需权衡性能与可读性。
示例代码(Java)
以下是一个简单的文本编辑器字符共享示例:
import java.util.HashMap;
import java.util.Map;
// 享元接口
interface Character {
void display(String position);
}
// 具体享元类
class ConcreteCharacter implements Character {
private final char symbol; // 内部状态(共享)
public ConcreteCharacter(char symbol) {
this.symbol = symbol;
}
@Override
public void display(String position) {
System.out.println("Character '" + symbol + "' at position: " + position);
}
}
// 享元工厂
class CharacterFactory {
private static final Map<Character, ConcreteCharacter> characters = new HashMap<>();
public static Character getCharacter(char symbol) {
return characters.computeIfAbsent(symbol, ConcreteCharacter::new);
}
}
// 客户端
public class Client {
public static void main(String[] args) {
Character a1 = CharacterFactory.getCharacter('A');
a1.display("(0,0)"); // 外部状态通过参数传递
Character a2 = CharacterFactory.getCharacter('A');
System.out.println("a1 == a2: " + (a1 == a2)); // 输出 true,验证共享
}
}
享元模式的优缺点对比
优点
-
减少内存占用
通过共享大量细粒度的对象,显著降低内存消耗,尤其是当系统中存在大量重复对象时。 -
提高性能
减少对象的创建和销毁次数,降低垃圾回收压力,从而提升程序运行效率。 -
外部状态独立
将对象的可变部分(外部状态)与不可变部分(内部状态)分离,便于灵活扩展和修改。 -
符合单一职责原则
将对象的共享逻辑与业务逻辑解耦,使代码结构更清晰。
缺点
-
增加系统复杂度
需要额外设计内部状态、外部状态的分离逻辑,并维护对象池(如工厂类),可能引入新的复杂性。 -
线程安全问题
多线程环境下,共享对象的修改(如外部状态)需额外同步处理,可能影响性能或引发竞态条件。 -
牺牲可读性
代码中需要区分内部状态和外部状态,对不熟悉模式的开发者可能难以理解。 -
适用场景有限
仅在存在大量重复对象且能分离状态时有效,若对象差异过大或无法共享,反而会增加冗余代码。
示例场景对比
- 适用场景:游戏中的子弹对象(内部状态为贴图,外部状态为坐标)、字符渲染(共享字体对象)。
- 不适用场景:对象间差异过大(如每个对象的内部状态均不同),或共享带来的复杂度超过内存节省的收益。
享元模式与其他设计模式的区别
1. 与单例模式的区别
- 目的不同
享元模式旨在通过共享对象减少内存消耗,而单例模式确保一个类只有一个实例并提供全局访问点。 - 对象数量
享元模式可以有多个共享对象(如不同颜色的棋子),单例模式严格限制为一个实例。 - 状态管理
享元对象通常包含内部状态(共享)和外部状态(非共享),单例对象的状态通常是全局统一的。
2. 与原型模式的区别
- 创建方式
享元模式通过工厂类复用现有对象,原型模式通过克隆现有对象生成新实例。 - 使用场景
享元模式适用于存在大量重复对象的场景(如字符渲染),原型模式适用于创建成本高的对象(如数据库连接)。
3. 与对象池模式的区别
- 生命周期
享元对象一旦创建便长期存在,对象池中的对象可被临时借用和归还(如数据库连接池)。 - 状态保持
享元对象的内部状态不可变,对象池中的对象可能每次被使用时状态重置。
4. 与组合模式的区别
- 结构关系
享元模式关注对象的共享,组合模式关注树形结构的对象组织(部分-整体关系)。 - 共享性
组合模式中的子节点通常是独立对象,享元模式中的子对象可被多个父对象共享。
5. 与装饰器模式的区别
- 功能扩展
装饰器模式动态添加职责,享元模式静态共享对象。 - 对象数量
装饰器模式可能生成大量包装对象,享元模式通过共享减少对象数量。
示例代码对比(享元 vs 单例)
// 享元模式示例:共享颜色对象
class Color {
private String name;
public Color(String name) { this.name = name; }
// getter...
}
class ColorFactory {
private static Map<String, Color> colors = new HashMap<>();
public static Color getColor(String name) {
return colors.computeIfAbsent(name, Color::new);
}
}
// 单例模式示例:唯一配置管理器
class ConfigManager {
private static ConfigManager instance = new ConfigManager();
private ConfigManager() {}
public static ConfigManager getInstance() { return instance; }
}
二、享元模式结构
Flyweight 接口
概念定义
Flyweight 接口是享元模式(Flyweight Pattern)中的核心组件之一,它定义了享元对象的公共接口。通过该接口,客户端可以访问享元对象的内部状态(Intrinsic State)和外部状态(Extrinsic State)。通常,Flyweight 接口是一个抽象类或接口,声明了享元对象的操作方法。
使用场景
- 共享对象:当系统中存在大量相似对象,且这些对象的大部分状态可以共享时,使用 Flyweight 接口可以显著减少内存占用。
- 不可变对象:享元对象通常是不可变的,Flyweight 接口确保对象的状态不会被意外修改。
- 外部状态管理:Flyweight 接口通常与外部状态(由客户端管理)结合使用,避免在享元对象中存储可变状态。
常见误区或注意事项
-
区分内部状态和外部状态:
- 内部状态(Intrinsic State)是享元对象共享的部分,存储在享元对象内部。
- 外部状态(Extrinsic State)是客户端特有的部分,由客户端在调用时传入。
- 误将外部状态存储在享元对象中会导致共享失效。
-
线程安全性:
- 如果享元对象需要被多线程访问,需确保其内部状态的线程安全性(通常享元对象是不可变的,因此天然线程安全)。
-
过度设计:
- 仅在对象数量极大且内存占用成为瓶颈时使用享元模式,否则会增加系统复杂性。
示例代码
以下是一个简单的 Flyweight 接口实现示例:
// Flyweight 接口
public interface Shape {
void draw(int x, int y); // x, y 是外部状态
}
// 具体享元类
public class Circle implements Shape {
private String color; // 内部状态(共享部分)
public Circle(String color) {
this.color = color;
}
@Override
public void draw(int x, int y) {
System.out.println("Drawing a " + color + " circle at (" + x + ", " + y + ")");
}
}
// 享元工厂
public class ShapeFactory {
private static final Map<String, Shape> circleMap = new HashMap<>();
public static Shape getCircle(String color) {
Shape circle = circleMap.get(color);
if (circle == null) {
circle = new Circle(color);
circleMap.put(color, circle);
}
return circle;
}
}
// 客户端代码
public class Client {
public static void main(String[] args) {
Shape redCircle = ShapeFactory.getCircle("red");
redCircle.draw(10, 20); // 外部状态 (10, 20)
Shape blueCircle = ShapeFactory.getCircle("blue");
blueCircle.draw(30, 40); // 外部状态 (30, 40)
// 复用已有的红色圆形
Shape anotherRedCircle = ShapeFactory.getCircle("red");
anotherRedCircle.draw(50, 60); // 外部状态 (50, 60)
}
}
关键点
Shape是 Flyweight 接口,定义了享元对象的操作(draw方法)。Circle是具体享元类,实现了Shape接口,并存储内部状态(color)。- 外部状态(
x,y)由客户端传入,避免在享元对象中存储可变数据。 ShapeFactory负责管理享元对象的创建和共享。
ConcreteFlyweight 具体实现
概念定义
ConcreteFlyweight(具体享元类)是享元模式中的核心实现类,负责存储和管理内部状态(Intrinsic State),即那些可以被多个对象共享的状态。它实现了 Flyweight 接口(或抽象类),并通过共享机制减少内存开销。
核心特点
- 内部状态共享:存储不变的数据(如字符、颜色等),这些数据可被多个上下文复用。
- 不可变设计:通常设计为不可变对象,避免共享状态被意外修改。
- 轻量化:仅包含必要的共享数据,不保存外部状态。
典型实现代码示例
// 1. 定义Flyweight接口
public interface Shape {
void draw(int x, int y); // x,y为外部状态(非共享)
}
// 2. 实现ConcreteFlyweight
public class Circle implements Shape {
private final String color; // 内部状态(共享)
public Circle(String color) {
this.color = color;
}
@Override
public void draw(int x, int y) {
System.out.printf("Drawing %s circle at (%d, %d)\n", color, x, y);
}
}
关键实现细节
-
工厂管理:通常与
FlyweightFactory配合使用,通过工厂控制实例的创建和复用。public class ShapeFactory { private static final Map<String, Shape> circleMap = new HashMap<>(); public static Shape getCircle(String color) { return circleMap.computeIfAbsent(color, Circle::new); } } -
外部状态处理:
- 外部状态(如坐标、尺寸)通过方法参数传入,而非存储在对象中。
- 示例中的
draw(int x, int y)方法接收外部状态。
使用场景
- 大量重复对象:如游戏中的子弹、粒子效果。
- 内存敏感场景:移动设备或嵌入式系统。
- 不可变数据共享:如字体渲染、棋盘棋子。
注意事项
-
线程安全:
- 如果享元对象需要修改内部状态(罕见),需加锁。
- 推荐设计为不可变对象(如示例中的
final color)。
-
外部状态隔离:
- 确保外部状态不会通过方法调用意外影响其他对象。
-
过度共享问题:
- 避免将本应独立的对象强行共享,导致逻辑复杂化。
完整调用示例
public class Client {
public static void main(String[] args) {
// 通过工厂获取共享对象
Shape redCircle = ShapeFactory.getCircle("Red");
Shape blueCircle = ShapeFactory.getCircle("Blue");
// 传递外部状态
redCircle.draw(10, 20);
blueCircle.draw(50, 100);
// 验证对象复用
System.out.println(ShapeFactory.getCircle("Red") == redCircle); // 输出 true
}
}
UnsharedConcreteFlyweight(非共享享元类)
概念定义
UnsharedConcreteFlyweight 是享元模式(Flyweight Pattern)中的一个特殊角色,它不被共享但同样实现了享元接口。与标准的 ConcreteFlyweight(共享享元类)不同,它的每个实例都保持独立状态,通常用于存储无法共享的特定对象数据。
核心特性
- 非共享性:每个实例独立存在,不参与享元池的共享机制
- 状态完整:同时包含内部状态(Intrinsic State)和外部状态(Extrinsic State)
- 接口一致性:与共享享元类实现相同的接口,保证模式统一性
使用场景
- 当某些对象本质上不可共享时(如需要保持唯一ID的对象)
- 处理特殊边界情况,需要独立实例时
- 作为组合对象中的非共享组件(如树形结构的非叶子节点)
代码示例
// 享元接口
interface Flyweight {
void operation(String extrinsicState);
}
// 共享享元类
class ConcreteFlyweight implements Flyweight {
private String intrinsicState;
public ConcreteFlyweight(String intrinsicState) {
this.intrinsicState = intrinsicState;
}
@Override
public void operation(String extrinsicState) {
System.out.println("共享对象: 内部状态[" + intrinsicState +
"], 外部状态[" + extrinsicState + "]");
}
}
// 非共享享元类
class UnsharedConcreteFlyweight implements Flyweight {
private String allState; // 同时包含内外状态
public UnsharedConcreteFlyweight(String allState) {
this.allState = allState;
}
@Override
public void operation(String extrinsicState) {
System.out.println("非共享对象: 完整状态[" + allState +
"], 额外参数[" + extrinsicState + "]");
}
}
// 客户端使用
public class Client {
public static void main(String[] args) {
Flyweight shared = new ConcreteFlyweight("共享状态A");
Flyweight unshared = new UnsharedConcreteFlyweight("独立状态X");
shared.operation("外部参数1"); // 使用共享对象
unshared.operation("外部参数2"); // 使用非共享对象
}
}
注意事项
- 内存权衡:非共享对象会消耗更多内存,应严格控制其数量
- 行为一致性:虽然不共享,但必须保证与共享对象相同的接口行为
- 工厂隔离:建议在FlyweightFactory中明确区分共享/非共享对象的创建逻辑
与共享享元的对比
| 特性 | UnsharedConcreteFlyweight | ConcreteFlyweight |
|---|---|---|
| 共享性 | 不共享 | 共享 |
| 状态存储 | 包含完整状态 | 仅内部状态 |
| 内存占用 | 较高 | 较低 |
| 适用场景 | 特殊/独立对象 | 可复用对象 |
FlyweightFactory 工厂类
概念定义
FlyweightFactory(享元工厂类)是享元模式中的核心组件,负责创建和管理享元对象。其主要职责包括:
- 维护享元池:内部维护一个存储享元对象的容器(通常使用HashMap或类似结构)
- 提供访问接口:当客户端请求享元对象时,工厂首先检查池中是否已存在该对象
- 控制对象创建:实现享元对象的复用逻辑,避免重复创建相同对象
核心特征
- 单例实现:通常设计为单例以确保全局唯一性
- 线程安全:需要考虑多线程环境下的并发访问问题
- 惰性加载:可按需创建享元对象而非一次性初始化
典型实现代码
public class FlyweightFactory {
// 享元池(线程安全实现)
private static final ConcurrentHashMap<String, Flyweight> pool = new ConcurrentHashMap<>();
// 获取享元对象(经典实现)
public static Flyweight getFlyweight(String intrinsicState) {
return pool.computeIfAbsent(intrinsicState, key -> {
System.out.println("创建新享元:" + key);
return new ConcreteFlyweight(key);
});
}
// 获取当前享元数量(调试用)
public static int getPoolSize() {
return pool.size();
}
}
关键方法解析
-
getFlyweight():
- 参数:接收内在状态(intrinsic state)作为键
- 逻辑:存在则返回已有对象,不存在则创建新对象并存入池中
- Java8+推荐使用
computeIfAbsent原子性操作
-
对象存储策略:
// 替代方案:显式同步控制 public synchronized Flyweight getFlyweight(String key) { Flyweight flyweight = pool.get(key); if (flyweight == null) { flyweight = new ConcreteFlyweight(key); pool.put(key, flyweight); } return flyweight; }
使用场景
- 大量细粒度对象:当系统需要创建大量相似对象时
- 内存敏感场景:如游戏开发中的粒子系统、文档编辑器中的字符对象
- 不可变对象:享元对象的状态应尽量设计为不可变
设计注意事项
-
线程安全实现:
- 推荐使用
ConcurrentHashMap(JDK1.5+) - 或使用
Collections.synchronizedMap包装 - 避免使用
Hashtable等过时实现
- 推荐使用
-
内存泄漏防范:
// 可添加清理机制 public static void removeFlyweight(String key) { pool.remove(key); } -
性能优化:
- 考虑使用弱引用(WeakReference)存储享元对象
- 对于高频访问对象可添加二级缓存
扩展实现示例(带销毁机制)
public class AdvancedFlyweightFactory {
private static final Map<String, SoftReference<Flyweight>> pool = new HashMap<>();
public static Flyweight getFlyweight(String key) {
synchronized (pool) {
SoftReference<Flyweight> ref = pool.get(key);
Flyweight flyweight = (ref != null) ? ref.get() : null;
if (flyweight == null) {
flyweight = new ConcreteFlyweight(key);
pool.put(key, new SoftReference<>(flyweight));
}
return flyweight;
}
}
public static void clearUnused() {
synchronized (pool) {
pool.entrySet().removeIf(entry -> entry.getValue().get() == null);
}
}
}
常见误区
-
混淆内在/外在状态:
- 错误:将可变状态存储在享元对象中
- 正确:应通过参数传递外在状态
-
过度使用单例:
- 不需要所有享元工厂都是单例
- 可根据业务需要创建多个工厂实例
-
忽视垃圾回收:
- 长期存活的享元对象可能进入老年代
- 对于生命周期明确的对象应提供销毁接口
客户端角色(Client Role)
概念定义
在享元模式(Flyweight Pattern)中,客户端角色负责维护对享元对象的引用,并根据需要请求享元工厂获取或创建享元对象。客户端通常不会直接实例化享元对象,而是通过享元工厂来管理共享的享元实例。
核心职责
- 请求享元对象:通过享元工厂获取共享的享元实例。
- 传递外部状态:享元模式区分内部状态(共享)和外部状态(非共享)。客户端负责计算或存储外部状态,并在调用享元对象时将其传入。
- 调用享元方法:客户端通过享元对象的接口执行操作,并传递必要的外部状态。
使用场景
客户端角色在以下场景中发挥作用:
- 需要大量细粒度对象时(如文本编辑器中的字符、游戏中的粒子效果)。
- 对象的大部分状态可以外部化,仅少部分需要共享。
示例代码
// 客户端代码示例
public class Client {
public static void main(String[] args) {
FlyweightFactory factory = new FlyweightFactory();
// 获取共享的享元对象并传递外部状态
Flyweight flyweight1 = factory.getFlyweight("shared_key");
flyweight1.operation("External State 1");
Flyweight flyweight2 = factory.getFlyweight("shared_key"); // 复用同一个实例
flyweight2.operation("External State 2");
}
}
注意事项
- 外部状态管理:客户端必须确保外部状态的正确性和一致性,享元对象不存储外部状态。
- 线程安全:多线程环境下,客户端需保证外部状态的线程安全(如通过局部变量或同步机制)。
- 性能权衡:过度使用享元模式可能导致客户端逻辑复杂化,需权衡共享带来的内存节省与代码复杂度。
常见误区
- 直接实例化享元对象:错误地绕过享元工厂创建对象,破坏共享机制。
- 混淆内外状态:将本应作为外部状态的属性误设计为享元的内部状态,导致无法共享。
三、享元模式实现
内部状态与外部状态
概念定义
在享元模式中,内部状态和外部状态是区分对象共享与非共享部分的关键概念:
-
内部状态(Intrinsic State)
- 存储在享元对象内部、不可变的数据。
- 可以被多个上下文共享,独立于具体场景。
- 例如:字符对象中的字符代码(如字母’A’的Unicode值)。
-
外部状态(Extrinsic State)
- 依赖于具体场景、可变的数据。
- 由客户端代码在调用时传入,享元对象不存储这部分状态。
- 例如:字符在文本中的位置(行号、列号)。
使用场景
- 内部状态用于标识对象的共享核心(如游戏中的相同3D模型)。
- 外部状态用于区分对象的个性化部分(如模型的位置、颜色)。
示例代码
// 享元接口
interface Character {
void display(String color); // color是外部状态
}
// 具体享元(内部状态为char类型)
class ConcreteCharacter implements Character {
private final char symbol; // 内部状态(不可变)
public ConcreteCharacter(char symbol) {
this.symbol = symbol;
}
@Override
public void display(String color) { // color由外部传入
System.out.println("Symbol: " + symbol + ", Color: " + color);
}
}
// 客户端使用
Character charA = new ConcreteCharacter('A');
charA.display("Red"); // 输出: Symbol: A, Color: Red
注意事项
- 线程安全
- 内部状态必须设计为不可变,避免多线程共享时的竞态条件。
- 外部状态管理
- 客户端需负责维护和传递外部状态,可能增加调用复杂度。
- 性能权衡
- 过度拆分状态可能导致频繁的参数传递,抵消共享带来的性能优势。
对象共享机制
概念定义
对象共享机制(Object Sharing Mechanism)是享元模式(Flyweight Pattern)的核心思想,旨在通过共享多个对象共有的相同部分,来减少内存消耗和提高性能。其核心是将对象分为内部状态(Intrinsic State)和外部状态(Extrinsic State):
- 内部状态:对象中不变的、可共享的部分,存储在享元对象内部。
- 外部状态:对象中变化的、不可共享的部分,由客户端在使用时传入。
使用场景
- 大量相似对象的场景:当系统中需要创建大量相似对象,且这些对象的大部分状态可以共享时(如游戏中的子弹、棋子、字符渲染等)。
- 内存敏感的应用:移动设备、嵌入式系统等内存资源受限的环境。
- 缓存池技术:如数据库连接池、线程池,本质上是享元模式的扩展应用。
实现步骤
- 分离内部状态与外部状态:将对象的可变部分(外部状态)和不可变部分(内部状态)解耦。
- 创建享元工厂:管理共享的享元对象,确保相同内部状态的对象只被创建一次。
- 客户端传递外部状态:客户端在使用享元对象时,动态传入外部状态。
示例代码
以下是一个简单的文本编辑器中使用享元模式共享字符对象的例子:
// 享元对象:字符对象(内部状态为字符本身)
class Character {
private final char charValue; // 内部状态(不可变)
public Character(char charValue) {
this.charValue = charValue;
}
public void display(int fontSize, String color) { // 外部状态由参数传入
System.out.printf("Character: %c, Font Size: %d, Color: %s%n",
charValue, fontSize, color);
}
}
// 享元工厂
class CharacterFactory {
private static final Map<Character, Character> pool = new HashMap<>();
public static Character getCharacter(char charValue) {
if (!pool.containsKey(charValue)) {
pool.put(charValue, new Character(charValue));
}
return pool.get(charValue);
}
}
// 客户端代码
public class Client {
public static void main(String[] args) {
String text = "Hello";
// 共享字符对象,仅外部状态(字体、颜色)不同
for (char c : text.toCharArray()) {
Character character = CharacterFactory.getCharacter(c);
character.display(12, "Black"); // 外部状态动态传入
}
}
}
常见误区与注意事项
- 过度共享问题:并非所有对象都适合共享。若对象的大部分状态是外部状态,可能导致代码复杂度增加。
- 线程安全问题:享元对象需设计为不可变(如内部状态用
final修饰),否则多线程环境下需额外同步。 - 外部状态管理:客户端需负责维护和传递外部状态,可能增加调用方的负担。
- 性能权衡:虽然减少了内存占用,但可能因频繁计算外部状态而增加 CPU 开销。
对比其他模式
- 与单例模式的区别:享元模式是“多例”的,根据内部状态区分不同对象;单例模式是全局唯一实例。
- 与对象池的区别:对象池关注复用对象的生命周期(如数据库连接),而享元模式关注共享不可变部分的状态。
享元模式中的线程安全考虑
概念定义
在享元模式中,线程安全指的是在多线程环境下,共享的享元对象能够被安全地访问和修改,而不会出现数据不一致或其他并发问题。由于享元对象是被多个客户端共享的,因此必须确保其内部状态(通常是不可变的)和外部状态(由客户端管理)在多线程环境下的正确性。
使用场景
- 不可变享元对象:如果享元对象的所有状态都是不可变的(即创建后不能被修改),则它是线程安全的,因为多个线程只能读取数据,不会发生竞争条件。
- 可变享元对象:如果享元对象包含可变状态,则需要通过同步机制(如锁、原子变量等)来保证线程安全。
常见误区或注意事项
- 忽略外部状态的线程安全:享元模式通常将外部状态交由客户端管理,但如果多个线程共享同一外部状态,可能会导致问题。客户端需要确保外部状态的线程安全。
- 过度同步:如果对享元对象的访问频繁加锁,可能会导致性能下降。应尽量设计为不可变对象,或使用更高效的并发控制机制(如读写锁)。
- 误用享元工厂:享元工厂通常需要保证线程安全,尤其是在创建和获取享元对象时。如果工厂不是线程安全的,可能会导致重复创建享元对象或数据不一致。
示例代码
以下是一个线程安全的享元模式实现示例,使用不可变享元对象和线程安全的工厂:
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
// 不可变享元对象
final class Flyweight {
private final String intrinsicState;
public Flyweight(String intrinsicState) {
this.intrinsicState = intrinsicState;
}
public void operation(String extrinsicState) {
System.out.println("Intrinsic State: " + intrinsicState + ", Extrinsic State: " + extrinsicState);
}
}
// 线程安全的享元工厂
class FlyweightFactory {
private static final Map<String, Flyweight> flyweights = new ConcurrentHashMap<>();
public static Flyweight getFlyweight(String key) {
return flyweights.computeIfAbsent(key, k -> new Flyweight(k));
}
}
// 客户端代码
public class Client {
public static void main(String[] args) {
Runnable task = () -> {
Flyweight flyweight = FlyweightFactory.getFlyweight("shared");
flyweight.operation(Thread.currentThread().getName());
};
// 模拟多线程环境
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
}
}
线程安全实现要点
- 享元对象不可变:
Flyweight类的intrinsicState是final的,确保创建后不会被修改。 - 线程安全工厂:
FlyweightFactory使用ConcurrentHashMap的computeIfAbsent方法,确保并发环境下享元对象的创建和获取是原子的。 - 外部状态管理:
operation方法的外部状态由客户端传入,不存储在享元对象中,避免了共享可变状态的问题。
享元模式中的缓存策略实现
概念定义
在享元模式中,缓存策略是指用于管理和重用享元对象(Flyweight)的机制。通过缓存已创建的享元对象,避免重复创建相同内在状态的对象,从而减少内存消耗和提高性能。
核心实现方式
1. 简单缓存(Map实现)
public class FlyweightFactory {
private static final Map<String, Flyweight> cache = new HashMap<>();
public static Flyweight getFlyweight(String intrinsicState) {
if (!cache.containsKey(intrinsicState)) {
cache.put(intrinsicState, new ConcreteFlyweight(intrinsicState));
}
return cache.get(intrinsicState);
}
}
2. LRU缓存策略
当需要限制缓存大小时,可采用最近最少使用策略:
public class FlyweightFactory {
private static final int MAX_SIZE = 100;
private static final LinkedHashMap<String, Flyweight> cache =
new LinkedHashMap<String, Flyweight>(MAX_SIZE, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > MAX_SIZE;
}
};
public static synchronized Flyweight getFlyweight(String key) {
Flyweight flyweight = cache.get(key);
if (flyweight == null) {
flyweight = new ConcreteFlyweight(key);
cache.put(key, flyweight);
}
return flyweight;
}
}
缓存策略选择
1. 静态缓存
- 适用于享元对象固定不变的场景
- 实现简单,无缓存淘汰机制
- 示例:字符编码转换器、数学常量对象
2. 动态缓存
- 使用LRU、LFU等淘汰算法
- 需要处理并发访问问题
- 示例:图形编辑器中的图元对象、游戏中的粒子效果
线程安全实现
1. 同步方法
public class FlyweightFactory {
private final Map<String, Flyweight> cache = new ConcurrentHashMap<>();
public Flyweight getFlyweight(String key) {
return cache.computeIfAbsent(key, ConcreteFlyweight::new);
}
}
2. 双重检查锁
public class FlyweightFactory {
private final Map<String, Flyweight> cache = new HashMap<>();
public Flyweight getFlyweight(String key) {
Flyweight result = cache.get(key);
if (result == null) {
synchronized (cache) {
result = cache.get(key);
if (result == null) {
result = new ConcreteFlyweight(key);
cache.put(key, result);
}
}
}
return result;
}
}
性能优化技巧
- 不可变对象:确保享元对象是不可变的,避免状态变化导致的缓存不一致
- 弱引用缓存:使用WeakHashMap防止内存泄漏
private final Map<String, WeakReference<Flyweight>> cache = new WeakHashMap<>();
- 分级缓存:对热点数据和使用频率低的数据采用不同缓存策略
典型应用场景
- 文本编辑器中的字符对象
- 游戏中的粒子系统
- 图形处理中的图元对象
- 数据库连接池管理
- 线程池中的线程复用
注意事项
- 对象相等性:确保作为缓存键的对象正确实现了hashCode()和equals()
- 内存监控:动态缓存需监控内存使用情况
- 清除策略:长期运行的系统需要定期清理不再使用的享元对象
- 序列化问题:缓存对象如需序列化,需考虑反序列化时的重建逻辑
对象池技术对比
概念定义
对象池技术是一种通过预先创建并管理一组可重用对象,以减少频繁创建和销毁对象带来的性能开销的优化手段。它通过复用已有对象来降低系统资源消耗,提高响应速度。
主要对象池实现方式对比
1. 简单对象池
public class SimpleObjectPool<T> {
private Queue<T> pool = new LinkedList<>();
public SimpleObjectPool(int size, Supplier<T> creator) {
for (int i = 0; i < size; i++) {
pool.offer(creator.get());
}
}
public T borrowObject() {
return pool.poll();
}
public void returnObject(T obj) {
pool.offer(obj);
}
}
特点:
- 最简单的实现方式
- 无超时控制
- 无对象有效性检查
- 适合简单的使用场景
2. Apache Commons Pool
GenericObjectPool<MyObject> pool = new GenericObjectPool<>(
new BasePooledObjectFactory<MyObject>() {
@Override
public MyObject create() {
return new MyObject();
}
}
);
特点:
- 成熟的开源实现
- 支持最大/最小空闲对象数配置
- 提供对象有效性检测
- 支持借出超时控制
- 支持JMX监控
3. ThreadLocal对象池
ThreadLocal<MyObject> threadLocal = ThreadLocal.withInitial(
() -> new MyObject()
);
特点:
- 线程级别对象复用
- 无锁竞争
- 对象生命周期与线程绑定
- 可能造成内存泄漏(需要及时remove)
对比维度
性能对比
| 类型 | 创建开销 | 并发性能 | 适用场景 |
|---|---|---|---|
| 简单对象池 | 低 | 中等 | 低并发简单场景 |
| Commons Pool | 中 | 高 | 高并发生产环境 |
| ThreadLocal | 最低 | 最高 | 线程封闭场景 |
功能对比
| 功能 | 简单对象池 | Commons Pool | ThreadLocal |
|---|---|---|---|
| 对象生命周期管理 | × | √ | × |
| 并发控制 | × | √ | √(无锁) |
| 有效性检测 | × | √ | × |
| 监控统计 | × | √ | × |
| 自动回收 | × | √ | × |
使用场景建议
-
简单对象池适合:
- 测试环境验证
- 对象创建成本不高
- 不需要复杂管理功能
-
Commons Pool适合:
- 生产环境
- 数据库连接池等资源管理
- 需要完善的管理功能
-
ThreadLocal适合:
- 线程封闭场景
- 极高并发要求
- 对象非线程安全需要隔离
注意事项
-
对象池大小需要合理配置:
- 太小会导致等待
- 太大会浪费内存
-
对象归还时:
- 必须重置对象状态
- 确保没有资源泄漏
-
对于ThreadLocal:
- 必须在线程结束时清理对象
- 避免在线程池环境中使用不当
-
对象池不是万能的:
- 轻量级对象可能不值得池化
- 有状态对象需要特别处理
四、享元模式应用
JDK 中的典型应用
字符串常量池
JDK 中最经典的享元模式实现是 字符串常量池(String Pool)。
- 原理:通过复用已存在的字符串对象,避免重复创建相同内容的字符串。
- 示例:
String s1 = "hello"; // 首次创建,加入常量池 String s2 = "hello"; // 直接复用常量池中的对象 System.out.println(s1 == s2); // 输出 true(地址相同) - 关键方法:
String.intern()强制将字符串加入常量池并返回引用。
包装类的缓存
JDK 对部分包装类(如 Integer、Long)的常用值范围做了缓存:
- 范围:
Integer默认缓存 -128~127 的对象。 - 示例:
Integer a = 127; // 从缓存获取 Integer b = 127; // 复用缓存对象 System.out.println(a == b); // true Integer c = 128; // 超出缓存范围,新建对象 Integer d = 128; System.out.println(c == d); // false - 原理:通过
Integer.valueOf()方法触发缓存机制,直接new Integer()则不会复用。
ThreadPool 中的线程复用
线程池(如 ThreadPoolExecutor)通过复用线程对象避免频繁创建/销毁:
- 享元体现:线程对象作为“享元”被多个任务共享使用。
- 优势:减少线程创建开销,提升性能。
其他案例
- 枚举类:JVM 保证每个枚举常量是单例,本质是享元。
- 日志框架:如 Log4j 中的
Level对象(如INFO、ERROR)被复用。
享元模式在图形编辑器中的应用
概念定义
享元模式(Flyweight Pattern)是一种结构型设计模式,它通过共享对象来最小化内存使用或计算开销。在图形编辑器中,享元模式特别适用于处理大量重复或相似的图形对象(如圆形、矩形等)。
使用场景
- 大量相似对象:当需要创建大量相似对象时(如文档中的相同字符、图形编辑器中的相同形状)。
- 对象状态可分离:对象的状态可以分为内部状态(Intrinsic State)和外部状态(Extrinsic State)。
- 内部状态:可共享的部分(如形状的类型、颜色)。
- 外部状态:不可共享的部分(如位置、大小)。
图形编辑器示例
假设我们有一个图形编辑器,用户可以添加大量圆形(Circle)和矩形(Rectangle)。每个图形的类型和颜色是固定的(内部状态),但位置和大小是可变的(外部状态)。
1. 定义享元接口
public interface Shape {
void draw(int x, int y, int width, int height); // 外部状态:位置和大小
}
2. 实现具体享元类
public class Circle implements Shape {
private String color; // 内部状态:颜色
public Circle(String color) {
this.color = color;
}
@Override
public void draw(int x, int y, int width, int height) {
System.out.println("Drawing Circle: Color=" + color +
", X=" + x + ", Y=" + y +
", Width=" + width + ", Height=" + height);
}
}
public class Rectangle implements Shape {
private String color; // 内部状态:颜色
public Rectangle(String color) {
this.color = color;
}
@Override
public void draw(int x, int y, int width, int height) {
System.out.println("Drawing Rectangle: Color=" + color +
", X=" + x + ", Y=" + y +
", Width=" + width + ", Height=" + height);
}
}
3. 享元工厂
import java.util.HashMap;
import java.util.Map;
public class ShapeFactory {
private static final Map<String, Shape> shapes = new HashMap<>();
public static Shape getShape(String type, String color) {
String key = type + "_" + color;
Shape shape = shapes.get(key);
if (shape == null) {
switch (type) {
case "Circle":
shape = new Circle(color);
break;
case "Rectangle":
shape = new Rectangle(color);
break;
}
shapes.put(key, shape);
}
return shape;
}
}
4. 客户端代码
public class GraphicEditor {
public static void main(String[] args) {
// 获取共享的圆形对象
Shape redCircle = ShapeFactory.getShape("Circle", "Red");
redCircle.draw(10, 10, 50, 50); // 外部状态:位置和大小
Shape blueCircle = ShapeFactory.getShape("Circle", "Blue");
blueCircle.draw(20, 20, 60, 60);
// 再次获取红色圆形(共享对象)
Shape anotherRedCircle = ShapeFactory.getShape("Circle", "Red");
anotherRedCircle.draw(30, 30, 70, 70);
// 验证对象是否共享
System.out.println("Is redCircle the same as anotherRedCircle? " +
(redCircle == anotherRedCircle)); // 输出 true
}
}
常见误区与注意事项
- 过度共享:并非所有对象都适合共享。如果外部状态过多或计算复杂,可能会抵消享元模式的优势。
- 线程安全:享元工厂通常是单例的,需确保线程安全(如使用
ConcurrentHashMap)。 - 内存泄漏:长期未使用的享元对象应及时清理(可通过弱引用或缓存策略实现)。
输出结果示例
Drawing Circle: Color=Red, X=10, Y=10, Width=50, Height=50
Drawing Circle: Color=Blue, X=20, Y=20, Width=60, Height=60
Drawing Circle: Color=Red, X=30, Y=30, Width=70, Height=70
Is redCircle the same as anotherRedCircle? true
享元模式在游戏开发中的应用
概念定义
享元模式(Flyweight Pattern)是一种结构型设计模式,通过共享对象来减少内存使用和提高性能。在游戏开发中,享元模式特别适用于需要大量重复对象的场景,例如粒子系统、子弹、敌人、地形块等。
使用场景
- 粒子系统:爆炸、烟雾、火焰等效果需要大量相似的粒子对象。
- 子弹和敌人:射击游戏中大量子弹或敌人实例。
- 地形和纹理:开放世界游戏中的重复地形块或纹理。
- 角色属性:多个NPC共享相同的属性(如移动速度、攻击力等)。
实现方式
享元模式通常包含以下角色:
- Flyweight(享元接口):定义共享对象的接口。
- ConcreteFlyweight(具体享元):实现享元接口,包含内部状态(可共享的部分)。
- FlyweightFactory(享元工厂):创建和管理享元对象,确保共享。
- Client(客户端):维护外部状态(不可共享的部分)。
示例代码
以下是一个简单的游戏子弹系统的享元模式实现:
// 享元接口
interface Bullet {
void shoot(int x, int y, int direction);
}
// 具体享元
class ConcreteBullet implements Bullet {
private String texture; // 内部状态(可共享)
private int damage;
public ConcreteBullet(String texture, int damage) {
this.texture = texture;
this.damage = damage;
}
@Override
public void shoot(int x, int y, int direction) {
System.out.println("Bullet with texture " + texture +
" shot at (" + x + "," + y + ") " +
"direction " + direction +
" with damage " + damage);
}
}
// 享元工厂
class BulletFactory {
private static final Map<String, Bullet> bulletCache = new HashMap<>();
public static Bullet getBullet(String texture, int damage) {
String key = texture + "_" + damage;
if (!bulletCache.containsKey(key)) {
bulletCache.put(key, new ConcreteBullet(texture, damage));
}
return bulletCache.get(key);
}
}
// 客户端使用
public class Game {
public static void main(String[] args) {
// 创建大量子弹,但只实际生成2个享元对象
Bullet bullet1 = BulletFactory.getBullet("red", 10);
Bullet bullet2 = BulletFactory.getBullet("blue", 20);
Bullet bullet3 = BulletFactory.getBullet("red", 10); // 重用bullet1对象
bullet1.shoot(100, 200, 45);
bullet2.shoot(150, 300, 90);
bullet3.shoot(200, 400, 180);
}
}
优化效果
- 内存节省:1000颗相同子弹只需1个享元对象。
- 性能提升:减少对象创建和垃圾回收开销。
- 维护方便:修改共享属性只需修改享元对象。
注意事项
- 线程安全:享元工厂需要考虑多线程访问问题。
- 状态分离:必须清晰区分内部状态(可共享)和外部状态(不可共享)。
- 过度使用:不是所有重复对象都适合享元模式,要考虑对象复用的实际频率。
游戏开发中的典型应用
- Unity引擎:Prefab系统本质上是享元模式的实现。
- 粒子系统:所有粒子共享相同的材质和物理属性。
- 棋牌游戏:相同的扑克牌或麻将牌面共享一个图形对象。
- RPG游戏:同类型怪物共享相同的模型和动画资源。
享元模式在文本处理中的应用
概念定义
享元模式(Flyweight Pattern)是一种结构型设计模式,它通过共享技术来高效地支持大量细粒度对象的复用。在文本处理中,享元模式可以显著减少内存消耗,特别是当处理大量重复字符或格式化元素时。
使用场景
- 文档编辑器:处理大量相同字符(如空格、标点符号)时
- 富文本处理:管理重复的文本样式(如字体、颜色)
- 日志分析:处理大量重复的日志消息模板
- 编译器实现:处理语言中的关键字和操作符
实现要点
- 将文本对象的内在状态(可共享部分)和外在状态(上下文相关部分)分离
- 使用工厂模式管理共享对象
- 通过哈希表等数据结构存储已创建的享元对象
示例代码
// 字符享元接口
interface CharacterFlyweight {
void display(int position);
}
// 具体字符享元
class ConcreteCharacter implements CharacterFlyweight {
private final char character;
public ConcreteCharacter(char c) {
this.character = c;
}
@Override
public void display(int position) {
System.out.println("Character '" + character + "' at position: " + position);
}
}
// 享元工厂
class CharacterFactory {
private static final Map<Character, CharacterFlyweight> characters = new HashMap<>();
public static CharacterFlyweight getCharacter(char c) {
return characters.computeIfAbsent(c, ConcreteCharacter::new);
}
}
// 客户端使用
public class TextProcessor {
public static void main(String[] args) {
String text = "Hello World";
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
CharacterFlyweight character = CharacterFactory.getCharacter(c);
character.display(i);
}
}
}
性能优化效果
假设处理100万个字符的文本:
- 传统方式:创建100万个独立字符对象
- 享元模式:只需创建不同字符的对象(ASCII字符只需128个实例)
注意事项
- 线程安全:享元工厂需要保证线程安全
- 状态管理:确保外在状态不会影响共享对象的行为
- 过度设计:对于简单场景可能增加不必要的复杂性
- 内存泄漏:长期持有的享元对象可能无法被GC回收
扩展应用
- 格式化文本处理:将字体、颜色等样式作为享元对象
- 正则表达式:共享正则模式对象
- 模板引擎:共享模板片段
- 词法分析:共享语言关键字token
数据库连接池实现
概念定义
数据库连接池是一种用于管理数据库连接的技术,它通过预先创建并维护一定数量的数据库连接,并在需要时分配给应用程序使用,从而避免了频繁创建和销毁连接的开销。连接池的核心目标是提高数据库访问性能、减少资源消耗。
使用场景
- 高并发应用:如电商网站、社交平台等需要频繁访问数据库的场景。
- 短生命周期连接:大量短时间使用的数据库连接,避免频繁创建和销毁。
- 资源受限环境:服务器资源有限,需要优化数据库连接的使用。
实现原理
- 初始化连接池:启动时创建一定数量的数据库连接并放入池中。
- 连接获取:应用程序从池中请求连接,如果池中有空闲连接则直接分配,否则等待或创建新连接(取决于配置)。
- 连接释放:使用完毕后,连接被返回到池中而不是直接关闭。
- 连接管理:包括连接有效性检测、超时处理、最大最小连接数控制等。
核心组件
- 连接池接口:定义获取、释放连接等基本操作。
- 连接对象包装:对原生连接进行包装,加入池化管理逻辑。
- 连接池配置:最大连接数、最小连接数、超时时间等参数。
- 连接状态管理:记录连接的使用状态(空闲、活跃等)。
示例代码(简单实现)
public class SimpleConnectionPool {
private static final int INITIAL_POOL_SIZE = 10;
private static final int MAX_POOL_SIZE = 20;
private static final long MAX_WAIT_TIME = 5000; // 5秒
private final BlockingQueue<Connection> pool;
private final String url;
private final String user;
private final String password;
private int currentPoolSize = 0;
public SimpleConnectionPool(String url, String user, String password) {
this.url = url;
this.user = user;
this.password = password;
this.pool = new LinkedBlockingQueue<>(MAX_POOL_SIZE);
initializePool();
}
private void initializePool() {
for (int i = 0; i < INITIAL_POOL_SIZE; i++) {
pool.add(createNewConnection());
}
}
private Connection createNewConnection() {
try {
currentPoolSize++;
return DriverManager.getConnection(url, user, password);
} catch (SQLException e) {
throw new RuntimeException("Failed to create new connection", e);
}
}
public Connection getConnection() throws InterruptedException {
Connection conn = pool.poll();
if (conn != null) {
return conn;
}
if (currentPoolSize < MAX_POOL_SIZE) {
return createNewConnection();
}
// 等待可用连接
conn = pool.poll(MAX_WAIT_TIME, TimeUnit.MILLISECONDS);
if (conn == null) {
throw new RuntimeException("Timeout waiting for connection");
}
return conn;
}
public void releaseConnection(Connection conn) {
if (conn != null) {
pool.offer(conn);
}
}
}
常见误区与注意事项
-
连接泄漏:未正确释放连接会导致连接耗尽。解决方案:
- 使用try-with-resources
- 实现连接包装器,在finalize方法中自动回收
-
连接有效性检测:
public boolean isValid(Connection conn, int timeout) { try { return conn != null && conn.isValid(timeout); } catch (SQLException e) { return false; } } -
配置参数不合理:
- 最大连接数过大:导致数据库负载过高
- 最小连接数过小:冷启动性能差
-
多线程安全问题:
- 确保连接获取和释放操作是线程安全的
- 使用并发集合如BlockingQueue
高级特性
- 连接预热:启动时预先创建最小连接数
- 动态扩容:根据负载自动调整连接数
- 监控统计:记录连接使用情况
- 多种驱逐策略:LRU、LFU等
流行实现对比
-
HikariCP:高性能,轻量级
HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/test"); config.setUsername("user"); config.setPassword("password"); config.setMaximumPoolSize(20); HikariDataSource ds = new HikariDataSource(config); -
Apache DBCP:功能全面
BasicDataSource ds = new BasicDataSource(); ds.setUrl("jdbc:mysql://localhost:3306/test"); ds.setUsername("user"); ds.setPassword("password"); ds.setMaxTotal(20); -
C3P0:老牌实现,稳定性高
ComboPooledDataSource ds = new ComboPooledDataSource(); ds.setJdbcUrl("jdbc:mysql://localhost:3306/test"); ds.setUser("user"); ds.setPassword("password"); ds.setMaxPoolSize(20);
性能优化建议
- 根据应用负载调整连接池大小
- 设置合理的连接超时时间
- 启用连接测试查询(testQuery)
- 考虑使用连接池监控工具
五、享元模式扩展
复合享元模式
概念定义
复合享元模式是享元模式的一种扩展形式,它允许将多个单纯享元对象组合成一个复合享元对象。复合享元对象本身也可以被共享,但其内部包含多个享元对象的组合。
与单纯享元模式的区别:
- 单纯享元:每个享元对象都是独立的、不可再分的
- 复合享元:由多个单纯享元组合而成,形成一个更大的可共享单元
使用场景
- 当需要表示由多个基本元素组成的复杂对象时
- 当这些组合对象也具备可共享特性时
- 典型应用场景:
- 文档编辑器中的段落(由多个字符组成)
- 游戏中的复杂地形(由多个基本图块组成)
- GUI中的复合控件(由多个简单控件组成)
实现方式
- 定义抽象享元接口
- 实现单纯享元类
- 实现复合享元类(包含一个单纯享元的集合)
- 使用工厂管理享元对象的创建和共享
示例代码
// 抽象享元接口
interface Flyweight {
void operation(String extrinsicState);
}
// 单纯享元
class ConcreteFlyweight implements Flyweight {
private String intrinsicState;
public ConcreteFlyweight(String intrinsicState) {
this.intrinsicState = intrinsicState;
}
@Override
public void operation(String extrinsicState) {
System.out.println("单纯享元: 内部状态[" + intrinsicState + "], 外部状态[" + extrinsicState + "]");
}
}
// 复合享元
class CompositeFlyweight implements Flyweight {
private Map<String, Flyweight> files = new HashMap<>();
public void add(String key, Flyweight fly) {
files.put(key, fly);
}
@Override
public void operation(String extrinsicState) {
System.out.println("--- 复合享元操作 ---");
files.forEach((k, v) -> v.operation(extrinsicState));
}
}
// 享元工厂
class FlyweightFactory {
private Map<String, Flyweight> flyweights = new HashMap<>();
public Flyweight getFlyweight(String key) {
Flyweight flyweight = flyweights.get(key);
if (flyweight == null) {
flyweight = new ConcreteFlyweight(key);
flyweights.put(key, flyweight);
}
return flyweight;
}
public Flyweight getCompositeFlyweight(List<String> keys) {
CompositeFlyweight composite = new CompositeFlyweight();
for (String key : keys) {
composite.add(key, this.getFlyweight(key));
}
return composite;
}
}
// 客户端使用
public class Client {
public static void main(String[] args) {
FlyweightFactory factory = new FlyweightFactory();
// 获取单纯享元
Flyweight f1 = factory.getFlyweight("A");
Flyweight f2 = factory.getFlyweight("B");
// 获取复合享元
Flyweight composite = factory.getCompositeFlyweight(Arrays.asList("A", "B", "C"));
f1.operation("1st call");
f2.operation("2nd call");
composite.operation("Composite call");
}
}
注意事项
- 复合享元的共享性:确保复合享元本身也是可共享的
- 组合关系管理:复合享元需要妥善管理其包含的单纯享元对象
- 外部状态处理:复合享元需要将外部状态传递给所有子享元
- 性能考量:虽然节省了内存,但组合操作可能增加运行时开销
优势与局限
优势:
- 进一步提高了对象的共享程度
- 可以表示更复杂的对象结构
- 保持了享元模式的内存节省优势
局限:
- 实现复杂度高于单纯享元模式
- 可能引入额外的性能开销
- 需要仔细设计复合对象的共享策略
带管理的享元模式
概念定义
带管理的享元模式(Managed Flyweight Pattern)是标准享元模式的扩展版本,它在原有享元模式的基础上增加了集中管理机制。核心思想是通过一个专门的享元工厂类统一管理享元对象的创建、缓存和复用,客户端不直接实例化享元对象,而是通过工厂获取。
关键组件
-
享元工厂(Flyweight Factory)
- 维护一个享元对象池(通常使用
HashMap) - 提供获取享元对象的接口(如
getFlyweight(key)) - 实现对象复用逻辑(存在则返回缓存,不存在则创建并缓存)
- 维护一个享元对象池(通常使用
-
抽象享元(Flyweight)
- 定义享元对象的接口
- 包含共享状态(内部状态)的操作方法
-
具体享元(Concrete Flyweight)
- 实现抽象享元接口
- 存储内部状态(可共享的部分)
-
非共享享元(UnsharedFlyweight,可选)
- 处理不需要共享的外部状态
典型代码实现
// 1. 抽象享元
interface Flyweight {
void operation(String extrinsicState);
}
// 2. 具体享元
class ConcreteFlyweight implements Flyweight {
private String intrinsicState; // 内部状态
public ConcreteFlyweight(String intrinsicState) {
this.intrinsicState = intrinsicState;
}
@Override
public void operation(String extrinsicState) {
System.out.println("内部状态: " + intrinsicState +
", 外部状态: " + extrinsicState);
}
}
// 3. 享元工厂(带管理功能)
class FlyweightFactory {
private Map<String, Flyweight> flyweights = new HashMap<>();
public Flyweight getFlyweight(String key) {
if (flyweights.containsKey(key)) {
return flyweights.get(key);
} else {
Flyweight fw = new ConcreteFlyweight(key);
flyweights.put(key, fw);
return fw;
}
}
public int getFlyweightCount() {
return flyweights.size();
}
}
// 客户端使用
public class Client {
public static void main(String[] args) {
FlyweightFactory factory = new FlyweightFactory();
Flyweight fw1 = factory.getFlyweight("A");
Flyweight fw2 = factory.getFlyweight("A"); // 复用
fw1.operation("第一次调用");
fw2.operation("第二次调用");
System.out.println("对象总数: " + factory.getFlyweightCount());
}
}
管理增强特性
-
对象生命周期管理
- 可实现LRU缓存淘汰策略
- 支持手动清理特定享元对象
-
线程安全控制
- 通过
synchronized或ConcurrentHashMap实现线程安全
- 通过
-
状态监控
- 统计缓存命中率
- 监控对象创建数量
使用场景
- 大量细粒度对象:如文档编辑器中的字符对象
- 内存敏感场景:移动设备应用、嵌入式系统
- 缓存系统:数据库连接池、线程池
- 游戏开发:粒子系统、纹理共享
注意事项
- 线程安全问题:多线程环境下需要同步控制
- 内存泄漏风险:长期不用的对象应及时清理
- 过度设计警告:对象数量较少时可能得不偿失
- 状态分离原则:必须严格区分内部状态和外部状态
性能优化方向
- 延迟初始化:首次请求时才创建对象
- 预加载机制:提前加载常用享元对象
- 分级缓存:按使用频率划分多级缓存池
- 弱引用管理:使用WeakReference防止内存泄漏
与标准享元模式的区别
| 特性 | 标准享元模式 | 带管理的享元模式 |
|---|---|---|
| 对象创建方式 | 客户端直接创建 | 通过工厂统一管理 |
| 缓存机制 | 需要自行实现 | 内置完善缓存管理 |
| 状态监控 | 不支持 | 可添加监控接口 |
| 线程安全性 | 需额外处理 | 工厂内部可内置安全机制 |
享元模式性能优化
享元模式(Flyweight Pattern)是一种结构型设计模式,旨在通过共享对象来减少内存使用和提高性能。它适用于系统中存在大量相似对象时,通过共享这些对象的公共部分来降低内存消耗。
核心思想
享元模式的核心在于区分内部状态(Intrinsic State)和外部状态(Extrinsic State):
- 内部状态:对象的共享部分,存储在享元对象内部,不会随环境变化。
- 外部状态:对象的非共享部分,由客户端在运行时传入,享元对象不存储这部分状态。
通过共享内部状态,减少重复对象的创建,从而优化性能。
使用场景
享元模式适用于以下场景:
- 大量相似对象:系统中存在大量相似对象,且这些对象的大部分状态可以共享。
- 内存敏感的应用:如游戏开发、图形渲染、文本编辑器等需要高效管理内存的场景。
- 不可变对象:享元对象通常是不可变的,因为共享状态需要保证线程安全。
典型应用:
- 游戏中的子弹、敌人等重复对象。
- 文本编辑器中的字符对象(如字母、数字)。
- 数据库连接池、线程池等资源池技术。
性能优化点
-
减少对象数量
通过共享内部状态,避免重复创建相同对象,从而降低内存占用和垃圾回收压力。 -
缓存机制
使用工厂类(如FlyweightFactory)缓存已创建的享元对象,避免重复实例化。 -
外部状态分离
将可变部分(外部状态)从对象中剥离,由客户端管理,进一步减少对象的内存占用。 -
延迟加载
仅在需要时创建享元对象,避免提前加载大量未使用的对象。
示例代码
以下是一个简单的享元模式实现示例,模拟文本编辑器中的字符渲染:
import java.util.HashMap;
import java.util.Map;
// 享元接口
interface CharacterFlyweight {
void display(String color);
}
// 具体享元类
class Character implements CharacterFlyweight {
private final char symbol; // 内部状态(共享)
public Character(char symbol) {
this.symbol = symbol;
}
@Override
public void display(String color) {
System.out.println("Character: " + symbol + ", Color: " + color); // 外部状态由参数传入
}
}
// 享元工厂
class CharacterFactory {
private static final Map<Character, CharacterFlyweight> pool = new HashMap<>();
public static CharacterFlyweight getCharacter(char symbol) {
if (!pool.containsKey(symbol)) {
pool.put(symbol, new Character(symbol)); // 缓存享元对象
}
return pool.get(symbol);
}
}
// 客户端
public class Client {
public static void main(String[] args) {
String text = "Hello, Flyweight!";
String[] colors = {"Red", "Green", "Blue"};
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
CharacterFlyweight character = CharacterFactory.getCharacter(c); // 获取共享对象
character.display(colors[i % colors.length]); // 传入外部状态(颜色)
}
}
}
输出示例:
Character: H, Color: Red
Character: e, Color: Green
Character: l, Color: Blue
Character: l, Color: Red
...
常见误区与注意事项
-
过度共享
不要将所有状态都设计为内部状态,否则会导致享元对象无法适应不同场景。需合理区分内外状态。 -
线程安全问题
如果享元对象需要修改内部状态(如缓存命中率统计),需确保线程安全(如使用ConcurrentHashMap)。 -
外部状态管理
客户端需负责维护外部状态,可能增加代码复杂度。可通过上下文类(如CharacterContext)封装外部状态。 -
不适合低频场景
如果对象复用率低,享元模式的性能优势可能无法抵消其设计复杂度。
性能对比
假设渲染 10,000 个字符:
- 普通模式:创建 10,000 个独立对象。
- 享元模式:仅创建 26 个字母对象(假设仅包含字母),内存占用显著降低。
通过共享对象,享元模式在内存和初始化时间上均有明显优化。
享元模式与单例模式的结合
概念定义
享元模式(Flyweight Pattern)和单例模式(Singleton Pattern)可以结合使用,以优化系统性能并确保对象的唯一性。享元模式的核心思想是通过共享大量细粒度的对象来减少内存消耗,而单例模式确保一个类只有一个实例,并提供全局访问点。结合使用时,通常将享元工厂设计为单例,或者将享元对象本身设计为单例。
使用场景
- 共享对象池:当系统中需要大量共享对象时,可以通过单例的享元工厂来管理这些对象,确保工厂的唯一性。
- 全局配置管理:例如,系统中的配置信息可以被设计为享元对象,并通过单例模式确保全局唯一。
- 线程池或连接池:池中的资源对象可以被设计为享元对象,而池管理器可以设计为单例。
常见误区或注意事项
- 线程安全问题:单例模式的享元工厂需要确保线程安全,尤其是在多线程环境下创建和访问享元对象时。
- 对象状态:享元对象通常是无状态的或状态是内蕴的(不可变),如果结合单例模式,需要特别注意对象的状态管理。
- 过度设计:不是所有场景都需要结合这两种模式,只有在确实需要共享对象且确保唯一性时才应考虑。
示例代码
以下是一个结合享元模式和单例模式的示例,展示如何通过单例的享元工厂管理享元对象:
import java.util.HashMap;
import java.util.Map;
// 享元接口
interface Flyweight {
void operation(String extrinsicState);
}
// 具体享元类
class ConcreteFlyweight implements Flyweight {
private String intrinsicState;
public ConcreteFlyweight(String intrinsicState) {
this.intrinsicState = intrinsicState;
}
@Override
public void operation(String extrinsicState) {
System.out.println("Intrinsic State: " + intrinsicState + ", Extrinsic State: " + extrinsicState);
}
}
// 单例的享元工厂
class FlyweightFactory {
private static FlyweightFactory instance = new FlyweightFactory();
private Map<String, Flyweight> flyweights = new HashMap<>();
private FlyweightFactory() {}
public static FlyweightFactory getInstance() {
return instance;
}
public Flyweight getFlyweight(String key) {
if (!flyweights.containsKey(key)) {
flyweights.put(key, new ConcreteFlyweight(key));
}
return flyweights.get(key);
}
}
// 客户端代码
public class Client {
public static void main(String[] args) {
FlyweightFactory factory = FlyweightFactory.getInstance();
Flyweight flyweight1 = factory.getFlyweight("A");
flyweight1.operation("First call");
Flyweight flyweight2 = factory.getFlyweight("A");
flyweight2.operation("Second call");
System.out.println("flyweight1 == flyweight2: " + (flyweight1 == flyweight2));
}
}
代码说明
- FlyweightFactory 是单例的,确保全局只有一个工厂实例。
- ConcreteFlyweight 是具体的享元对象,通过工厂的
getFlyweight方法获取。 - 客户端通过单例工厂获取享元对象,多次请求相同的键会返回同一个享元对象,从而实现了对象的共享。
输出结果
Intrinsic State: A, Extrinsic State: First call
Intrinsic State: A, Extrinsic State: Second call
flyweight1 == flyweight2: true
通过这种方式,享元模式和单例模式的结合可以高效地管理共享对象,并确保系统的性能与一致性。
分布式环境下的享元模式应用
概念定义
享元模式(Flyweight Pattern)是一种结构型设计模式,旨在通过共享对象来减少内存使用和提高性能。在分布式环境中,享元模式的应用更加复杂,需要考虑网络通信、数据一致性和并发访问等问题。
使用场景
- 缓存共享对象:在分布式系统中,某些对象(如配置信息、元数据)可以被多个节点共享,避免重复创建和存储。
- 减少网络传输:通过共享对象,可以减少节点间的数据传输量,降低网络开销。
- 提高性能:共享对象可以减少内存占用和对象创建的开销,从而提高系统整体性能。
常见误区或注意事项
- 线程安全问题:在分布式环境中,多个节点可能同时访问共享对象,需要确保线程安全。
- 数据一致性:共享对象的状态需要在所有节点间保持一致,避免脏读或数据不一致。
- 网络延迟:共享对象的访问可能涉及网络通信,需要考虑网络延迟对性能的影响。
- 对象生命周期管理:共享对象的生命周期需要妥善管理,避免内存泄漏或过早释放。
示例代码
以下是一个简单的分布式环境下享元模式的实现示例,使用Java和Redis作为共享存储:
import redis.clients.jedis.Jedis;
public class DistributedFlyweight {
private static final String REDIS_HOST = "localhost";
private static final int REDIS_PORT = 6379;
private Jedis jedis;
public DistributedFlyweight() {
this.jedis = new Jedis(REDIS_HOST, REDIS_PORT);
}
// 获取共享对象
public String getSharedObject(String key) {
String value = jedis.get(key);
if (value == null) {
value = createObject(key); // 模拟创建对象
jedis.set(key, value);
}
return value;
}
// 模拟创建对象
private String createObject(String key) {
return "Object_" + key;
}
public static void main(String[] args) {
DistributedFlyweight flyweight = new DistributedFlyweight();
String obj1 = flyweight.getSharedObject("key1");
String obj2 = flyweight.getSharedObject("key1");
System.out.println("obj1: " + obj1);
System.out.println("obj2: " + obj2);
System.out.println("obj1 == obj2: " + (obj1 == obj2)); // 注意:字符串常量池可能影响结果
}
}
优化建议
- 使用本地缓存:结合本地缓存(如Guava Cache)和分布式缓存(如Redis),减少网络访问。
- 实现对象池:对于创建成本高的对象,可以实现对象池来管理共享对象。
- 考虑序列化:在分布式环境中,共享对象可能需要序列化和反序列化,选择高效的序列化方式(如Protobuf、Kryo)。
- 监控和调优:监控共享对象的使用情况,及时调整缓存策略和对象生命周期。
通过合理应用享元模式,可以在分布式环境中显著提高系统性能和资源利用率。
272

被折叠的 条评论
为什么被折叠?



