引言:一场内存危机的启示
想象一下这个场景:你正在开发一个大型文档编辑器,需要渲染成千上万个字符。如果每个字符都创建一个独立的对象,包含字体、大小、颜色等完整属性,你的内存很快就会被耗尽。这正是享元模式要解决的核心问题------在大量细粒度对象共享时,如何有效减少内存占用。
享元模式(Flyweight Pattern)是23种设计模式中的结构型模式,它的核心思想是:运用共享技术来有效支持大量细粒度的对象。今天,我将带你深入探索享元模式的Java实现,从基础概念到高级应用。
一、享元模式的核心思想
1.1 两个关键概念
内部状态(Intrinsic State):
- 对象共享的部分
- 不会随环境改变
- 存储在享元对象内部
- 示例:字符的Unicode编码、棋子的颜色
外部状态(Extrinsic State):
- 对象特有的部分
- 随环境变化
- 由客户端维护
- 示例:字符的位置、棋子的坐标
1.2 模式结构
┌─────────────────────────────────────┐
│ 客户端 (Client) │
│ ┌─────────────────────────┐ │
│ │ 享元工厂 (Flyweight │ │
│ │ Factory) │ │
│ └───────────┬─────────────┘ │
│ │ │
│ ┌───────────┼─────────────┐ │
│ ▼ ▼ ▼ │
│ 具体享元 具体享元 享元接口 │
│ (Concrete (Concrete (Flyweight)│
│ Flyweight) Flyweight) │
└─────────────────────────────────────┘
二、基础实现:文字编辑器案例
让我们从一个简单的文字编辑器开始,看看享元模式如何优化内存使用。
2.1 问题场景
我们需要渲染一个包含10,000个字符的文档。如果每个字符都包含完整属性:
java
// 传统方式 - 内存杀手
class Character {
private char value;
private String fontFamily;
private int fontSize;
private String color;
private int positionX;
private int positionY;
// ... 其他属性
// 10,000个这样的对象会消耗大量内存!
}
2.2 享元模式解决方案
java
// 1. 享元接口 - 定义共享操作
public interface CharacterFlyweight {
void display(CharacterContext context);
}
// 2. 具体享元 - 存储内部状态
public class ConcreteCharacter implements CharacterFlyweight {
private final char character;
private final String fontFamily;
private final int fontSize;
private final String color;
// 内部状态在构造时确定,之后不可变
public ConcreteCharacter(char character, String fontFamily,
int fontSize, String color) {
this.character = character;
this.fontFamily = fontFamily;
this.fontSize = fontSize;
this.color = color;
}
@Override
public void display(CharacterContext context) {
// 使用内部状态(共享)和外部状态(特有)进行渲染
System.out.printf("渲染字符 '%c',字体: %s,大小: %d,颜色: %s,位置: (%d, %d)%n",
character, fontFamily, fontSize, color,
context.getX(), context.getY());
}
// 内部状态的getter
public char getCharacter() { return character; }
public String getFontFamily() { return fontFamily; }
public int getFontSize() { return fontSize; }
public String getColor() { return color; }
}
// 3. 外部状态封装
public class CharacterContext {
private final int x;
private final int y;
public CharacterContext(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
}
// 4. 享元工厂 - 核心的共享机制
public class CharacterFactory {
// 使用Map缓存已创建的享元对象
private static final Map<String, ConcreteCharacter> characterPool =
new ConcurrentHashMap<>();
// 创建字符的唯一入口
public static ConcreteCharacter getCharacter(char c, String fontFamily,
int fontSize, String color) {
// 构建唯一键 - 内部状态的组合
String key = buildKey(c, fontFamily, fontSize, color);
// 如果已存在,直接返回
ConcreteCharacter character = characterPool.get(key);
if (character != null) {
return character;
}
// 不存在则创建并缓存
character = new ConcreteCharacter(c, fontFamily, fontSize, color);
characterPool.put(key, character);
System.out.println("创建新字符享元: " + key + ",缓存大小: " +
characterPool.size());
return character;
}
private static String buildKey(char c, String fontFamily,
int fontSize, String color) {
return c + "|" + fontFamily + "|" + fontSize + "|" + color;
}
// 获取缓存统计信息
public static int getCacheSize() {
return characterPool.size();
}
public static void clearCache() {
characterPool.clear();
}
}
// 5. 客户端使用
public class TextEditor {
private final List<Pair<ConcreteCharacter, CharacterContext>> document =
new ArrayList<>();
public void addCharacter(char c, String fontFamily, int fontSize,
String color, int x, int y) {
// 获取享元对象(可能来自缓存)
ConcreteCharacter character = CharacterFactory.getCharacter(
c, fontFamily, fontSize, color);
// 创建外部状态
CharacterContext context = new CharacterContext(x, y);
// 存储引用关系
document.add(new Pair<>(character, context));
}
public void render() {
System.out.println("开始渲染文档,共" + document.size() + "个字符");
System.out.println("享元对象数量: " + CharacterFactory.getCacheSize());
for (Pair<ConcreteCharacter, CharacterContext> entry : document) {
ConcreteCharacter character = entry.getKey();
CharacterContext context = entry.getValue();
character.display(context);
}
}
// 内存使用对比
public void memoryComparison() {
// 传统方式:每个字符都创建完整对象
int traditionalMemory = document.size() * estimateCharacterSize();
// 享元模式:共享内部状态
int flyweightMemory = CharacterFactory.getCacheSize() *
estimateCharacterSize() +
document.size() * estimateContextSize();
System.out.printf("\n内存使用对比:\n");
System.out.printf("传统方式: %d bytes\n", traditionalMemory);
System.out.printf("享元模式: %d bytes\n", flyweightMemory);
System.out.printf("节省内存: %.1f%%\n",
(1 - (double)flyweightMemory / traditionalMemory) * 100);
}
private int estimateCharacterSize() {
// 估算一个Character对象的大小(简化)
return 64; // bytes
}
private int estimateContextSize() {
// 估算Context对象的大小
return 16; // bytes
}
}
// 测试类
public class FlyweightDemo {
public static void main(String[] args) {
TextEditor editor = new TextEditor();
// 模拟添加10,000个字符
Random random = new Random();
String fonts = "Arial,宋体,微软雅黑,Times New Roman";
String[] fontArray = fonts.split(",");
String colors = "black,red,blue,green";
String[] colorArray = colors.split(",");
for (int i = 0; i < 10000; i++) {
char c = (char) ('A' + random.nextInt(26));
String font = fontArray[random.nextInt(fontArray.length)];
int size = 12 + random.nextInt(6); // 12-17px
String color = colorArray[random.nextInt(colorArray.length)];
int x = random.nextInt(1000);
int y = random.nextInt(1000);
editor.addCharacter(c, font, size, color, x, y);
}
// 渲染文档
editor.render();
// 显示内存对比
editor.memoryComparison();
}
}
运行结果示例:
创建新字符享元: A|Arial|12|black,缓存大小: 1
创建新字符享元: B|宋体|14|red,缓存大小: 2
...(更多的创建日志)
开始渲染文档,共10000个字符
享元对象数量: 48
渲染字符 'A',字体: Arial,大小: 12,颜色: black,位置: (123, 456)
渲染字符 'B',字体: 宋体,大小: 14,颜色: red,位置: (789, 321)
...
内存使用对比:
传统方式: 640000 bytes
享元模式: 4864 bytes
节省内存: 99.2%
三、进阶应用:围棋棋子享元实现
让我们看一个更复杂的例子------围棋游戏。围棋有361个交叉点,但只有黑白两种棋子。
3.1 完整实现
java
// 1. 享元接口
public interface ChessPiece {
void place(Coordinate coordinate);
Color getColor();
}
// 2. 具体享元
public class ConcreteChessPiece implements ChessPiece {
private final Color color; // 内部状态:颜色
private final String shape; // 内部状态:形状
public ConcreteChessPiece(Color color) {
this.color = color;
this.shape = "圆形"; // 固定形状
}
@Override
public void place(Coordinate coordinate) {
System.out.printf("%s棋子放置在位置(%d, %d)%n",
color == Color.BLACK ? "黑" : "白",
coordinate.getX(), coordinate.getY());
}
@Override
public Color getColor() {
return color;
}
}
// 3. 享元工厂(带线程安全)
public class ChessPieceFactory {
private static final Map<Color, ChessPiece> piecePool =
new ConcurrentHashMap<>();
public static ChessPiece getPiece(Color color) {
// 双重检查锁定(虽然ConcurrentHashMap不需要,这里展示模式)
ChessPiece piece = piecePool.get(color);
if (piece == null) {
synchronized (ChessPieceFactory.class) {
piece = piecePool.get(color);
if (piece == null) {
piece = new ConcreteChessPiece(color);
piecePool.put(color, piece);
System.out.println("创建" +
(color == Color.BLACK ? "黑" : "白") + "棋子享元");
}
}
}
return piece;
}
public static int getPieceCount() {
return piecePool.size();
}
}
// 4. 棋盘管理
public class ChessBoard {
private final ChessPiece[][] board;
private static final int BOARD_SIZE = 19;
public ChessBoard() {
board = new ChessPiece[BOARD_SIZE][BOARD_SIZE];
}
public void placePiece(Color color, int x, int y) {
if (x < 0 || x >= BOARD_SIZE || y < 0 || y >= BOARD_SIZE) {
throw new IllegalArgumentException("位置超出棋盘范围");
}
if (board[x][y] != null) {
throw new IllegalStateException("该位置已有棋子");
}
// 获取享元棋子
ChessPiece piece = ChessPieceFactory.getPiece(color);
board[x][y] = piece;
// 放置棋子
piece.place(new Coordinate(x, y));
}
public void display() {
System.out.println("\n当前棋盘状态:");
for (int i = 0; i < BOARD_SIZE; i++) {
for (int j = 0; j < BOARD_SIZE; j++) {
if (board[i][j] == null) {
System.out.print("+");
} else if (board[i][j].getColor() == Color.BLACK) {
System.out.print("●");
} else {
System.out.print("○");
}
}
System.out.println();
}
System.out.println("使用的享元对象数量: " +
ChessPieceFactory.getPieceCount());
}
}
// 5. 测试游戏
public class ChessGame {
public static void main(String[] args) {
ChessBoard board = new ChessBoard();
Random random = new Random();
// 模拟下棋
for (int i = 0; i < 50; i++) {
Color color = i % 2 == 0 ? Color.BLACK : Color.WHITE;
int x = random.nextInt(19);
int y = random.nextInt(19);
try {
board.placePiece(color, x, y);
} catch (IllegalStateException e) {
// 位置已有棋子,重试
i--;
}
}
board.display();
}
}
四、享元模式在JDK和框架中的应用
4.1 JDK中的享元模式
Integer缓存
java
public class IntegerCacheExample {
public static void main(String[] args) {
// Integer的享元模式实现
Integer a = Integer.valueOf(127);
Integer b = Integer.valueOf(127);
System.out.println(a == b); // true,同一个对象
Integer c = Integer.valueOf(128);
Integer d = Integer.valueOf(128);
System.out.println(c == d); // false,超出缓存范围
// 查看缓存范围
System.out.println("Integer缓存范围: " +
IntegerCache.low + " 到 " + IntegerCache.high);
}
// Integer源码中的缓存实现
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
int h = 127;
// 可以通过JVM参数调整上限
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch(NumberFormatException nfe) {
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
}
}
}
String常量池
java
public class StringPoolExample {
public static void main(String[] args) {
// String常量池是享元模式的典型应用
String s1 = "Hello";
String s2 = "Hello";
System.out.println(s1 == s2); // true,同一个对象
String s3 = new String("Hello");
String s4 = new String("Hello");
System.out.println(s3 == s4); // false,不同对象
// intern()方法可以放入常量池
String s5 = s3.intern();
System.out.println(s1 == s5); // true
}
}
4.2 Spring框架中的享元应用
java
// Spring中的线程池配置共享
@Configuration
public class ThreadPoolConfig {
@Bean("sharedThreadPool")
public ExecutorService sharedThreadPool() {
// 这个线程池被多个服务共享使用
return Executors.newFixedThreadPool(10);
}
}
@Service
public class OrderService {
@Autowired
@Qualifier("sharedThreadPool")
private ExecutorService threadPool; // 共享的享元对象
}
@Service
public class UserService {
@Autowired
@Qualifier("sharedThreadPool")
private ExecutorService threadPool; // 同一个享元对象
}
五、性能优化与最佳实践
5.1 内存优化策略
java
/**
* 享元对象池的优化实现
* 1. 软引用缓存
* 2. LRU淘汰策略
* 3. 并发控制优化
*/
public class OptimizedFlyweightFactory<K, V> {
private final Map<K, SoftReference<V>> cache;
private final LinkedHashMap<K, Long> accessLog;
private final int maxSize;
private final ReentrantReadWriteLock lock;
public OptimizedFlyweightFactory(int maxSize) {
this.maxSize = maxSize;
this.cache = new ConcurrentHashMap<>();
this.accessLog = new LinkedHashMap<>(16, 0.75f, true);
this.lock = new ReentrantReadWriteLock();
}
public V get(K key, Supplier<V> creator) {
// 读锁(允许多个线程同时读取)
lock.readLock().lock();
try {
SoftReference<V> ref = cache.get(key);
V value = ref != null ? ref.get() : null;
if (value != null) {
// 记录访问时间(用于LRU)
recordAccess(key);
return value;
}
} finally {
lock.readLock().unlock();
}
// 缓存未命中,获取写锁
lock.writeLock().lock();
try {
// 双重检查
SoftReference<V> ref = cache.get(key);
V value = ref != null ? ref.get() : null;
if (value != null) {
recordAccess(key);
return value;
}
// 创建新对象
value = creator.get();
cache.put(key, new SoftReference<>(value));
recordAccess(key);
// 清理过期引用
cleanUp();
return value;
} finally {
lock.writeLock().unlock();
}
}
private void recordAccess(K key) {
accessLog.put(key, System.currentTimeMillis());
}
private void cleanUp() {
if (cache.size() > maxSize) {
// LRU淘汰
Iterator<Map.Entry<K, Long>> it = accessLog.entrySet().iterator();
while (it.hasNext() && cache.size() > maxSize * 0.75) {
Map.Entry<K, Long> entry = it.next();
// 淘汰最久未使用的
if (System.currentTimeMillis() - entry.getValue() > 60000) {
cache.remove(entry.getKey());
it.remove();
}
}
}
}
// 内存监控
public void printMemoryStats() {
Runtime runtime = Runtime.getRuntime();
long used = runtime.totalMemory() - runtime.freeMemory();
System.out.printf("内存使用: %dMB, 缓存大小: %d%n",
used / 1024 / 1024, cache.size());
}
}
5.2 线程安全实现
java
/**
* 线程安全的享元工厂
*/
public class ThreadSafeFlyweightFactory {
// 方案1:使用ConcurrentHashMap(推荐)
private final Map<String, Object> cache1 = new ConcurrentHashMap<>();
public Object getFlyweight1(String key) {
return cache1.computeIfAbsent(key, k -> createFlyweight(k));
}
// 方案2:双重检查锁定(适用于非并发集合)
private final Map<String, Object> cache2 = new HashMap<>();
public Object getFlyweight2(String key) {
Object flyweight = cache2.get(key);
if (flyweight == null) {
synchronized (cache2) {
flyweight = cache2.get(key);
if (flyweight == null) {
flyweight = createFlyweight(key);
cache2.put(key, flyweight);
}
}
}
return flyweight;
}
// 方案3:使用Guava Cache
private final Cache<String, Object> cache3 = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterAccess(10, TimeUnit.MINUTES)
.build();
public Object getFlyweight3(String key) throws ExecutionException {
return cache3.get(key, () -> createFlyweight(key));
}
private Object createFlyweight(String key) {
// 创建享元对象的逻辑
return new Object();
}
}
六、享元模式的局限性
6.1 何时不使用享元模式
java
// 反模式示例:不应该使用享元的情况
public class WrongFlyweightUse {
// 情况1:对象变化频繁
class Player {
private String name; // 变化频繁
private int health; // 变化频繁
private Position position; // 变化频繁
// 这些属性经常变化,不适合作为内部状态
}
// 情况2:对象数量很少
// 如果只有几个对象,享元模式带来的复杂性不值得
// 情况3:外部状态管理复杂
class ComplexContext {
// 如果外部状态过于复杂,管理成本可能超过节省的内存
private Map<String, Object> states;
private List<Dependency> dependencies;
private Configuration config;
// ... 太多外部状态
}
}
6.2 识别合适的场景
适合使用享元模式的情况:
- ✅ 系统中存在大量相似对象
- ✅ 对象的大部分状态可以外部化
- ✅ 使用享元后能显著减少内存占用
- ✅ 对象标识不重要(可以共享)
不适合使用享元模式的情况:
- ❌ 对象需要频繁变化状态
- ❌ 对象的数量很少
- ❌ 外部状态过于复杂
- ❌ 需要维护对象间的独立关系
七、现代应用:连接池与缓存
7.1 数据库连接池的享元实现
java
/**
* 数据库连接池 - 享元模式的现代应用
*/
public class DatabaseConnectionPool {
private final List<Connection> pool;
private final List<Connection> usedConnections;
private final int maxSize;
public DatabaseConnectionPool(String url, String user,
String password, int maxSize) throws SQLException {
this.maxSize = maxSize;
this.pool = new ArrayList<>(maxSize);
this.usedConnections = new ArrayList<>();
// 预先创建连接
for (int i = 0; i < maxSize; i++) {
pool.add(DriverManager.getConnection(url, user, password));
}
}
public synchronized Connection getConnection() throws InterruptedException {
while (pool.isEmpty()) {
if (usedConnections.size() < maxSize) {
// 可以创建新连接
try {
pool.add(createNewConnection());
} catch (SQLException e) {
throw new RuntimeException(e);
}
} else {
// 等待连接释放
wait();
}
}
Connection connection = pool.remove(pool.size() - 1);
usedConnections.add(connection);
return new PooledConnection(connection);
}
public synchronized boolean releaseConnection(Connection connection) {
if (connection instanceof PooledConnection) {
PooledConnection pooled = (PooledConnection) connection;
usedConnections.remove(pooled.getRealConnection());
pool.add(pooled.getRealConnection());
notifyAll();
return true;
}
return false;
}
private Connection createNewConnection() throws SQLException {
// 实际项目中从配置读取
return DriverManager.getConnection(
"jdbc:mysql://localhost:3306/test",
"root",
"password"
);
}
// 包装类,用于正确释放连接
private class PooledConnection implements Connection {
private final Connection realConnection;
PooledConnection(Connection realConnection) {
this.realConnection = realConnection;
}
Connection getRealConnection() {
return realConnection;
}
@Override
public void close() throws SQLException {
releaseConnection(this);
}
// 委托所有Connection方法给realConnection
@Override
public Statement createStatement() throws SQLException {
return realConnection.createStatement();
}
// ... 其他方法委托
}
}
7.2 与Spring框架集成
java
@Configuration
public class FlyweightConfiguration {
@Bean
@Scope("singleton")
public FlyweightFactory flyweightFactory() {
return new FlyweightFactory();
}
@Bean
@Scope("prototype") // 每次获取新实例
public ClientService clientService() {
return new ClientService();
}
}
@Service
public class ClientService {
@Autowired
private FlyweightFactory factory;
public void process() {
// 使用享元对象
Flyweight flyweight = factory.getFlyweight("key");
flyweight.operation(new ExternalState());
}
}
// AOP监控享元使用情况
@Aspect
@Component
public class FlyweightMonitor {
@Around("execution(* FlyweightFactory.getFlyweight(..))")
public Object monitorFlyweightUsage(ProceedingJoinPoint joinPoint) throws Throwable {
String key = (String) joinPoint.getArgs()[0];
long start = System.currentTimeMillis();
try {
Object result = joinPoint.proceed();
long duration = System.currentTimeMillis() - start;
// 记录指标
Metrics.recordFlyweightAccess(key, duration,
result instanceof CachedFlyweight);
return result;
} catch (Exception e) {
Metrics.recordFlyweightError(key);
throw e;
}
}
}
八、性能测试与对比
java
public class FlyweightBenchmark {
public static void main(String[] args) {
int objectCount = 100000;
// 测试传统方式
long traditionalTime = testTraditional(objectCount);
long traditionalMemory = getUsedMemory();
// 测试享元模式
long flyweightTime = testFlyweight(objectCount);
long flyweightMemory = getUsedMemory();
System.out.println("\n===== 性能测试结果 =====");
System.out.printf("对象数量: %,d%n", objectCount);
System.out.printf("传统方式 - 时间: %,d ms, 内存: %,d bytes%n",
traditionalTime, traditionalMemory);
System.out.printf("享元模式 - 时间: %,d ms, 内存: %,d bytes%n",
flyweightTime, flyweightMemory);
System.out.printf("时间提升: %.1f%%%n",
(1 - (double)flyweightTime / traditionalTime) * 100);
System.out.printf("内存节省: %.1f%%%n",
(1 - (double)flyweightMemory / traditionalMemory) * 100);
}
private static long testTraditional(int count) {
List<TraditionalObject> list = new ArrayList<>(count);
long start = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
TraditionalObject obj = new TraditionalObject(
"key" + (i % 100), // 只有100种不同的内部状态
i, i * 2, i * 3 // 外部状态各不相同
);
list.add(obj);
}
return System.currentTimeMillis() - start;
}
private static long testFlyweight(int count) {
List<Pair<Flyweight, ExternalState>> list = new ArrayList<>(count);
FlyweightFactory factory = new FlyweightFactory();
long start = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
Flyweight flyweight = factory.getFlyweight("key" + (i % 100));
ExternalState state = new ExternalState(i, i * 2, i * 3);
list.add(new Pair<>(flyweight, state));
}
return System.currentTimeMillis() - start;
}
private static long getUsedMemory() {
Runtime runtime = Runtime.getRuntime();
runtime.gc(); // 建议GC,但不确定会立即执行
return runtime.totalMemory() - runtime.freeMemory();
}
}
// 测试类定义
class TraditionalObject {
private final String intrinsicState;
private final int extrinsic1;
private final int extrinsic2;
private final int extrinsic3;
public TraditionalObject(String intrinsicState, int e1, int e2, int e3) {
this.intrinsicState = intrinsicState;
this.extrinsic1 = e1;
this.extrinsic2 = e2;
this.extrinsic3 = e3;
}
}
class Flyweight {
private final String intrinsicState;
public Flyweight(String intrinsicState) {
this.intrinsicState = intrinsicState;
}
}
总结
享元模式通过共享对象的内在状态,显著减少了大量相似对象的内存占用。它特别适用于以下场景:
- 大量相似对象存在:如文档编辑器中的字符、棋类游戏中的棋子
- 对象的大部分状态可以外部化:如位置、方向等可以独立存储
- 内存是主要约束:在内存受限的环境中特别有效
关键收获:
- 分清内部状态(共享)和外部状态(特有)
- 使用工厂模式管理享元对象的创建和缓存
- 注意线程安全和性能优化
- 结合现代框架(如Spring)和工具(如Guava Cache)使用