深入理解设计模式之享元模式(Flyweight Pattern)
一、引言
在软件开发中,我们经常需要创建大量相似的对象。比如:一个文档编辑器中有成千上万个字符对象;一个游戏中有数百万个相同类型的子弹;一个网站要为数千个用户创建会话对象。如果为每个对象都分配独立的内存空间,会造成巨大的内存开销。
想象一下这个场景:你正在开发一个围棋游戏,棋盘上有361个位置,每个位置可能放置黑子或白子。如果为每个棋子都创建一个独立的对象,假设每个对象占用100字节,那么一盘棋最多需要36,100字节。但实际上,所有黑子看起来都一样,所有白子也都一样,它们唯一的区别只是位置不同。如果我们只创建一个黑子对象和一个白子对象,然后通过不同的位置参数来复用它们,内存开销会大大降低。
享元模式就是为了解决这类问题而诞生的一种结构型设计模式。它通过共享技术有效地支持大量细粒度对象的复用,从而减少内存消耗,提高系统性能。
二、什么是享元模式
2.1 定义
享元模式(Flyweight Pattern)是一种结构型设计模式,它使用共享对象的方法来支持大量细粒度对象的复用。享元模式通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似类的开销,从而提高系统资源的利用率。
2.2 核心思想
- 对象共享:多个地方使用相同的对象实例
- 状态分离 :将对象状态分为内部状态和外部状态
- 内部状态(Intrinsic State):存储在享元对象内部,可以共享,不会随环境变化
- 外部状态(Extrinsic State):随环境变化而变化,不可共享,由客户端保存和传递
- 工厂管理:使用工厂模式管理享元对象池
- 延迟创建:需要时才创建,创建后缓存复用
2.3 模式结构
享元模式结构:
┌──────────────────────────┐
│ Client │ 客户端
└────────────┬─────────────┘
│ 使用
▼
┌──────────────────────────┐
│ FlyweightFactory │ 享元工厂
├──────────────────────────┤
│ - flyweights: Map │ 享元池
│ + getFlyweight(key) │ 获取享元对象
└────────────┬─────────────┘
│ 创建和管理
▼
┌──────────────────────────┐
│ <<interface>> │
│ Flyweight │ 抽象享元
├──────────────────────────┤
│ + operation(extrinsic) │ 传入外部状态
└────────────┬─────────────┘
△
│ 实现
┌───────┴────────┐
│ │
┌────┴──────────┐ ┌──┴─────────────┐
│Concrete │ │Unshared │
│Flyweight │ │Flyweight │
├───────────────┤ ├────────────────┤
│- intrinsic │ │- allState │
│ State │ │ (不共享) │
└───────────────┘ └────────────────┘
共享对象 不共享对象
内存对比:
不使用享元模式:
┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐
│对象│ │对象│ │对象│ │对象│ │对象│ 1000个对象
│ A │ │ A │ │ A │ │ A │ │ A │ = 1000份内存
└────┘ └────┘ └────┘ └────┘ └────┘
使用享元模式:
┌────┐
所有引用→│对象│ 1个共享对象
│ A │ = 1份内存
└────┘
↑ ↑ ↑ ↑ ↑
└────┴────┴────┴────┘
1000个引用指向同一对象
2.4 享元模式的关键
内部状态 vs 外部状态:
┌─────────────────────────────────┐
│ 棋子对象 │
├─────────────────────────────────┤
│ 内部状态(可共享): │
│ - 颜色(黑/白) │
│ - 形状 │
│ - 大小 │
│ │
│ 外部状态(不可共享): │
│ - 位置坐标 (x, y) │
│ - 当前玩家 │
└─────────────────────────────────┘
享元对象只存储内部状态
外部状态由客户端传入
三、基础示例
3.1 场景:围棋游戏
经典的享元模式示例:围棋棋子只有黑白两种,但位置各不相同。
抽象享元:
java
/**
* 抽象享元:棋子接口
*/
public interface ChessPiece {
/**
* 放置棋子
* @param x 横坐标(外部状态)
* @param y 纵坐标(外部状态)
*/
void place(int x, int y);
}
具体享元:
java
/**
* 具体享元:黑色棋子
*/
public class BlackChessPiece implements ChessPiece {
// 内部状态:颜色(所有黑子共享)
private final String color = "黑色";
public BlackChessPiece() {
System.out.println("创建黑色棋子对象");
}
@Override
public void place(int x, int y) {
// 外部状态:位置(由外部传入)
System.out.println("在位置 (" + x + ", " + y + ") 放置" + color + "棋子");
}
}
/**
* 具体享元:白色棋子
*/
public class WhiteChessPiece implements ChessPiece {
// 内部状态:颜色(所有白子共享)
private final String color = "白色";
public WhiteChessPiece() {
System.out.println("创建白色棋子对象");
}
@Override
public void place(int x, int y) {
// 外部状态:位置(由外部传入)
System.out.println("在位置 (" + x + ", " + y + ") 放置" + color + "棋子");
}
}
享元工厂:
java
import java.util.HashMap;
import java.util.Map;
/**
* 享元工厂:管理棋子对象池
*/
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) {
// 不存在则创建
if ("black".equalsIgnoreCase(color)) {
piece = new BlackChessPiece();
} else if ("white".equalsIgnoreCase(color)) {
piece = new WhiteChessPiece();
}
// 放入池中
if (piece != null) {
pool.put(color, piece);
}
}
return piece;
}
/**
* 获取对象池大小
*/
public static int getPoolSize() {
return pool.size();
}
}
客户端使用:
java
public class ChessGameDemo {
public static void main(String[] args) {
System.out.println("========== 开始下围棋 ==========\n");
// 第1步:黑子
ChessPiece black1 = ChessPieceFactory.getChessPiece("black");
black1.place(3, 3);
// 第2步:白子
ChessPiece white1 = ChessPieceFactory.getChessPiece("white");
white1.place(3, 4);
// 第3步:黑子(复用已有对象)
ChessPiece black2 = ChessPieceFactory.getChessPiece("black");
black2.place(4, 3);
// 第4步:白子(复用已有对象)
ChessPiece white2 = ChessPieceFactory.getChessPiece("white");
white2.place(4, 4);
// 第5步:黑子
ChessPiece black3 = ChessPieceFactory.getChessPiece("black");
black3.place(5, 5);
// 验证对象复用
System.out.println("\n========== 验证对象复用 ==========");
System.out.println("black1 == black2: " + (black1 == black2));
System.out.println("black1 == black3: " + (black1 == black3));
System.out.println("white1 == white2: " + (white1 == white2));
System.out.println("对象池大小: " + ChessPieceFactory.getPoolSize());
// 内存节省计算
System.out.println("\n========== 内存节省分析 ==========");
System.out.println("如果一盘棋有200步:");
System.out.println("不使用享元模式:需要创建200个对象");
System.out.println("使用享元模式:只需创建2个对象(黑、白各1个)");
System.out.println("节省对象数量:198个(99%)");
}
}
输出:
========== 开始下围棋 ==========
创建黑色棋子对象
在位置 (3, 3) 放置黑色棋子
创建白色棋子对象
在位置 (3, 4) 放置白色棋子
在位置 (4, 3) 放置黑色棋子
在位置 (4, 4) 放置白色棋子
在位置 (5, 5) 放置黑色棋子
========== 验证对象复用 ==========
black1 == black2: true
black1 == black3: true
white1 == white2: true
对象池大小: 2
========== 内存节省分析 ==========
如果一盘棋有200步:
不使用享元模式:需要创建200个对象
使用享元模式:只需创建2个对象(黑、白各1个)
节省对象数量:198个(99%)
3.2 场景:图形绘制
在图形编辑软件中,可能需要绘制大量相同的图形。
java
/**
* 抽象享元:图形接口
*/
interface Shape {
void draw(int x, int y, int size, String color);
}
/**
* 具体享元:圆形
*/
class Circle implements Shape {
// 内部状态:类型
private final String type = "圆形";
public Circle() {
System.out.println("创建圆形对象");
}
@Override
public void draw(int x, int y, int size, String color) {
// 外部状态:位置、大小、颜色
System.out.println("绘制" + type + " - 位置:(" + x + "," + y +
"), 大小:" + size + ", 颜色:" + color);
}
}
/**
* 具体享元:矩形
*/
class Rectangle implements Shape {
// 内部状态:类型
private final String type = "矩形";
public Rectangle() {
System.out.println("创建矩形对象");
}
@Override
public void draw(int x, int y, int size, String color) {
System.out.println("绘制" + type + " - 位置:(" + x + "," + y +
"), 大小:" + size + ", 颜色:" + color);
}
}
/**
* 享元工厂
*/
class ShapeFactory {
private static final Map<String, Shape> shapePool = new HashMap<>();
public static Shape getShape(String shapeType) {
Shape shape = shapePool.get(shapeType);
if (shape == null) {
switch (shapeType.toLowerCase()) {
case "circle":
shape = new Circle();
break;
case "rectangle":
shape = new Rectangle();
break;
}
if (shape != null) {
shapePool.put(shapeType, shape);
}
}
return shape;
}
public static int getPoolSize() {
return shapePool.size();
}
}
/**
* 测试
*/
class GraphicsDemo {
public static void main(String[] args) {
System.out.println("========== 绘制多个图形 ==========\n");
// 绘制多个圆形
for (int i = 0; i < 3; i++) {
Shape circle = ShapeFactory.getShape("circle");
circle.draw(i * 10, i * 10, 5, "红色");
}
System.out.println();
// 绘制多个矩形
for (int i = 0; i < 3; i++) {
Shape rectangle = ShapeFactory.getShape("rectangle");
rectangle.draw(i * 20, i * 20, 10, "蓝色");
}
System.out.println("\n========== 统计 ==========");
System.out.println("总共绘制了6个图形");
System.out.println("实际创建的对象数量: " + ShapeFactory.getPoolSize());
}
}
四、实际生产场景应用
4.1 场景:数据库连接池
数据库连接池是享元模式的典型应用。
java
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
* 模拟数据库连接
*/
class DatabaseConnection {
private String id;
private boolean inUse;
public DatabaseConnection(String id) {
this.id = id;
this.inUse = false;
System.out.println("创建数据库连接: " + id);
}
public void execute(String sql) {
System.out.println("连接 " + id + " 执行SQL: " + sql);
}
public String getId() { return id; }
public boolean isInUse() { return inUse; }
public void setInUse(boolean inUse) { this.inUse = inUse; }
}
/**
* 数据库连接池(享元工厂)
*/
class ConnectionPool {
private static final int MAX_POOL_SIZE = 10;
private static final int INITIAL_POOL_SIZE = 3;
// 空闲连接池
private Queue<DatabaseConnection> idleConnections = new ConcurrentLinkedQueue<>();
// 使用中的连接
private Set<DatabaseConnection> activeConnections = new HashSet<>();
private int connectionCount = 0;
public ConnectionPool() {
// 初始化连接池
for (int i = 0; i < INITIAL_POOL_SIZE; i++) {
DatabaseConnection conn = new DatabaseConnection("CONN-" + (++connectionCount));
idleConnections.offer(conn);
}
System.out.println("连接池初始化完成,初始连接数: " + INITIAL_POOL_SIZE);
}
/**
* 获取连接(享元对象)
*/
public synchronized DatabaseConnection getConnection() {
DatabaseConnection conn = null;
// 1. 尝试从空闲池获取
conn = idleConnections.poll();
// 2. 如果没有空闲连接,且未达到最大连接数,创建新连接
if (conn == null && (activeConnections.size() + idleConnections.size()) < MAX_POOL_SIZE) {
conn = new DatabaseConnection("CONN-" + (++connectionCount));
System.out.println("连接池扩容,当前总连接数: " + connectionCount);
}
// 3. 如果获取到连接,标记为使用中
if (conn != null) {
conn.setInUse(true);
activeConnections.add(conn);
System.out.println("获取连接: " + conn.getId() + ",当前活动连接: " + activeConnections.size());
} else {
System.out.println("连接池已满,无法获取连接");
}
return conn;
}
/**
* 释放连接(归还到池中)
*/
public synchronized void releaseConnection(DatabaseConnection conn) {
if (conn != null && activeConnections.remove(conn)) {
conn.setInUse(false);
idleConnections.offer(conn);
System.out.println("释放连接: " + conn.getId() + ",当前活动连接: " + activeConnections.size());
}
}
/**
* 获取池状态
*/
public synchronized String getPoolStatus() {
return "连接池状态 - 总连接: " + connectionCount +
", 活动: " + activeConnections.size() +
", 空闲: " + idleConnections.size();
}
}
/**
* 测试连接池
*/
class ConnectionPoolDemo {
public static void main(String[] args) {
ConnectionPool pool = new ConnectionPool();
System.out.println("\n========== 场景1:正常使用 ==========");
// 获取连接
DatabaseConnection conn1 = pool.getConnection();
conn1.execute("SELECT * FROM users");
DatabaseConnection conn2 = pool.getConnection();
conn2.execute("INSERT INTO logs VALUES (...)");
System.out.println(pool.getPoolStatus());
// 释放连接
pool.releaseConnection(conn1);
pool.releaseConnection(conn2);
System.out.println(pool.getPoolStatus());
System.out.println("\n========== 场景2:连接复用 ==========");
// 再次获取连接(会复用之前释放的连接)
DatabaseConnection conn3 = pool.getConnection();
System.out.println("conn3 == conn1: " + (conn3 == conn1));
conn3.execute("UPDATE users SET status = 1");
pool.releaseConnection(conn3);
System.out.println("\n========== 场景3:并发获取 ==========");
// 模拟并发获取多个连接
List<DatabaseConnection> connections = new ArrayList<>();
for (int i = 0; i < 5; i++) {
DatabaseConnection conn = pool.getConnection();
if (conn != null) {
connections.add(conn);
}
}
System.out.println(pool.getPoolStatus());
// 释放所有连接
for (DatabaseConnection conn : connections) {
pool.releaseConnection(conn);
}
System.out.println(pool.getPoolStatus());
}
}
4.2 场景:线程池
线程池也是享元模式的应用。
java
import java.util.concurrent.*;
/**
* 线程池示例(JDK提供的ThreadPoolExecutor)
*/
class ThreadPoolExample {
public static void main(String[] args) throws InterruptedException {
// 创建线程池(享元工厂)
// 核心线程数:3,最大线程数:5
ThreadPoolExecutor executor = new ThreadPoolExecutor(
3, // 核心线程数
5, // 最大线程数
60, // 空闲线程存活时间
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10) // 任务队列
);
System.out.println("========== 提交10个任务 ==========\n");
// 提交10个任务
for (int i = 1; i <= 10; i++) {
final int taskId = i;
executor.execute(() -> {
System.out.println("任务 " + taskId + " 由线程 " +
Thread.currentThread().getName() + " 执行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 等待任务完成
Thread.sleep(3000);
System.out.println("\n========== 线程池统计 ==========");
System.out.println("核心线程数: " + executor.getCorePoolSize());
System.out.println("当前线程数: " + executor.getPoolSize());
System.out.println("完成任务数: " + executor.getCompletedTaskCount());
// 关闭线程池
executor.shutdown();
System.out.println("\n享元模式的体现:");
System.out.println("1. 10个任务共享3-5个线程对象");
System.out.println("2. 线程对象复用,避免频繁创建销毁");
System.out.println("3. 节省系统资源,提高性能");
}
}
4.3 场景:对象池
通用对象池实现。
java
import java.util.*;
/**
* 可重用对象接口
*/
interface Reusable {
void reset(); // 重置对象状态
}
/**
* 通用对象池
*/
class ObjectPool<T extends Reusable> {
private Queue<T> pool = new LinkedList<>();
private int maxSize;
private java.util.function.Supplier<T> factory;
public ObjectPool(int maxSize, java.util.function.Supplier<T> factory) {
this.maxSize = maxSize;
this.factory = factory;
}
/**
* 获取对象
*/
public synchronized T acquire() {
T obj = pool.poll();
if (obj == null) {
// 池中没有对象,创建新对象
obj = factory.get();
System.out.println("创建新对象: " + obj.getClass().getSimpleName());
} else {
System.out.println("从池中获取对象: " + obj.getClass().getSimpleName());
}
return obj;
}
/**
* 归还对象
*/
public synchronized void release(T obj) {
if (obj != null && pool.size() < maxSize) {
obj.reset(); // 重置对象状态
pool.offer(obj);
System.out.println("对象归还到池中");
}
}
public int getPoolSize() {
return pool.size();
}
}
/**
* 示例:StringBuilder对象池
*/
class PooledStringBuilder implements Reusable {
private StringBuilder sb = new StringBuilder();
public PooledStringBuilder append(String str) {
sb.append(str);
return this;
}
@Override
public String toString() {
return sb.toString();
}
@Override
public void reset() {
sb.setLength(0); // 清空内容
}
}
/**
* 测试对象池
*/
class ObjectPoolDemo {
public static void main(String[] args) {
// 创建StringBuilder对象池,最大容量5
ObjectPool<PooledStringBuilder> pool =
new ObjectPool<>(5, PooledStringBuilder::new);
System.out.println("========== 使用对象池 ==========\n");
// 第一次使用
PooledStringBuilder sb1 = pool.acquire();
sb1.append("Hello");
System.out.println("结果1: " + sb1);
pool.release(sb1);
System.out.println();
// 第二次使用(复用对象)
PooledStringBuilder sb2 = pool.acquire();
System.out.println("sb1 == sb2: " + (sb1 == sb2));
sb2.append("World");
System.out.println("结果2: " + sb2);
pool.release(sb2);
System.out.println();
// 并发使用多个对象
List<PooledStringBuilder> builders = new ArrayList<>();
for (int i = 0; i < 3; i++) {
PooledStringBuilder sb = pool.acquire();
sb.append("Task" + i);
builders.add(sb);
}
System.out.println();
// 归还所有对象
for (PooledStringBuilder sb : builders) {
pool.release(sb);
}
System.out.println("\n池中对象数量: " + pool.getPoolSize());
}
}
五、JDK中的享元模式
5.1 String常量池
String是享元模式最经典的应用。
java
/**
* String常量池示例
*/
public class StringPoolDemo {
public static void main(String[] args) {
System.out.println("========== String常量池 ==========\n");
// 字符串字面量存储在常量池中,会被复用
String s1 = "Hello";
String s2 = "Hello";
String s3 = "Hello";
System.out.println("s1 == s2: " + (s1 == s2)); // true
System.out.println("s1 == s3: " + (s1 == s3)); // true
System.out.println("三个变量指向同一个对象");
System.out.println();
// new String()会创建新对象
String s4 = new String("Hello");
System.out.println("s1 == s4: " + (s1 == s4)); // false
// intern()方法返回常量池中的对象
String s5 = s4.intern();
System.out.println("s1 == s5: " + (s1 == s5)); // true
System.out.println("\n========== 享元模式的体现 ==========");
System.out.println("1. 相同内容的字符串共享同一个对象");
System.out.println("2. 节省内存空间");
System.out.println("3. 提高字符串比较效率(可以用==比较)");
}
}
5.2 Integer缓存池
Integer对-128到127的值进行了缓存。
java
/**
* Integer缓存池示例
*/
public class IntegerPoolDemo {
public static void main(String[] args) {
System.out.println("========== Integer缓存池 ==========\n");
// -128到127之间的值会被缓存
Integer i1 = 100;
Integer i2 = 100;
Integer i3 = 100;
System.out.println("i1 == i2: " + (i1 == i2)); // true
System.out.println("i1 == i3: " + (i1 == i3)); // true
System.out.println("三个变量指向同一个对象");
System.out.println();
// 超出缓存范围,会创建新对象
Integer i4 = 200;
Integer i5 = 200;
System.out.println("i4 == i5: " + (i4 == i5)); // false
System.out.println("超出缓存范围,创建新对象");
System.out.println();
// valueOf()使用缓存
Integer i6 = Integer.valueOf(100);
System.out.println("i1 == i6: " + (i1 == i6)); // true
// new Integer()总是创建新对象
Integer i7 = new Integer(100);
System.out.println("i1 == i7: " + (i1 == i7)); // false
System.out.println("\n========== 源码分析 ==========");
System.out.println("Integer.valueOf()实现:");
System.out.println("if (i >= -128 && i <= 127)");
System.out.println(" return IntegerCache.cache[i + 128];");
System.out.println("return new Integer(i);");
}
}
/**
* 简化的Integer缓存实现
*/
class SimpleInteger {
private final int value;
// 缓存数组
private static class Cache {
static final SimpleInteger[] cache = new SimpleInteger[256];
static {
// 缓存-128到127
for (int i = 0; i < cache.length; i++) {
cache[i] = new SimpleInteger(i - 128);
}
}
}
private SimpleInteger(int value) {
this.value = value;
}
/**
* 工厂方法(享元工厂)
*/
public static SimpleInteger valueOf(int i) {
if (i >= -128 && i <= 127) {
// 返回缓存的对象
return Cache.cache[i + 128];
}
// 超出范围,创建新对象
return new SimpleInteger(i);
}
public int intValue() {
return value;
}
}
5.3 Boolean单例
Boolean只有TRUE和FALSE两个实例。
java
/**
* Boolean享元模式
*/
public class BooleanDemo {
public static void main(String[] args) {
System.out.println("========== Boolean享元模式 ==========\n");
Boolean b1 = Boolean.valueOf(true);
Boolean b2 = Boolean.valueOf(true);
Boolean b3 = Boolean.TRUE;
System.out.println("b1 == b2: " + (b1 == b2)); // true
System.out.println("b1 == b3: " + (b1 == b3)); // true
System.out.println("所有true值共享Boolean.TRUE对象");
System.out.println();
Boolean b4 = Boolean.valueOf(false);
Boolean b5 = Boolean.FALSE;
System.out.println("b4 == b5: " + (b4 == b5)); // true
System.out.println("所有false值共享Boolean.FALSE对象");
System.out.println("\n========== 源码分析 ==========");
System.out.println("Boolean只有两个实例:");
System.out.println("public static final Boolean TRUE = new Boolean(true);");
System.out.println("public static final Boolean FALSE = new Boolean(false);");
System.out.println();
System.out.println("valueOf()实现:");
System.out.println("return (b ? TRUE : FALSE);");
}
}
六、享元模式的优缺点
6.1 优点
1. 大幅减少对象数量
示例:1000个棋子
不使用享元:1000个对象
使用享元:2个对象(黑、白各1个)
节省:99.8%
2. 降低内存占用
假设每个对象占用100字节:
不使用享元:1000 × 100 = 100KB
使用享元:2 × 100 = 200字节
节省:99.8% 内存
3. 提高系统性能
- 减少GC压力
- 减少对象创建开销
- 提高缓存命中率
4. 外部状态相对独立
内部状态不变,外部状态灵活变化
同一个对象可以有不同的外部状态
6.2 缺点
1. 增加系统复杂度
需要分离内部状态和外部状态
需要维护对象池
逻辑变得复杂
2. 外部状态管理成本
外部状态需要由客户端维护
如果外部状态很多,反而增加开销
3. 线程安全问题
共享对象需要考虑并发访问
可能需要同步机制
影响性能
4. 运行时查找开销
从对象池中查找对象需要时间
如果对象种类很多,查找效率降低
七、最佳实践
7.1 何时使用享元模式
java
/**
* 使用享元模式的判断标准
*/
class FlyweightCriteria {
/**
* 满足以下条件时考虑使用享元模式:
*
* 1. 系统中存在大量相似对象(数量级:成百上千)
* 2. 对象可以分为内部状态和外部状态
* 3. 内部状态相同的对象可以共享
* 4. 外部状态可以被剥离出来
* 5. 对象创建成本较高
* 6. 内存资源有限
*/
public static boolean shouldUseFlyweight(
int objectCount,
boolean hasIntrinsicState,
boolean hasExtrinsicState,
boolean highCreationCost
) {
return objectCount > 100 &&
hasIntrinsicState &&
hasExtrinsicState &&
highCreationCost;
}
}
7.2 线程安全的享元工厂
java
import java.util.concurrent.ConcurrentHashMap;
/**
* 线程安全的享元工厂
*/
class ThreadSafeFlyweightFactory {
// 使用ConcurrentHashMap保证线程安全
private static final ConcurrentHashMap<String, ChessPiece> pool =
new ConcurrentHashMap<>();
/**
* 双重检查锁定
*/
public static ChessPiece getChessPiece(String color) {
ChessPiece piece = pool.get(color);
if (piece == null) {
synchronized (ThreadSafeFlyweightFactory.class) {
piece = pool.get(color);
if (piece == null) {
// 创建新对象
if ("black".equalsIgnoreCase(color)) {
piece = new BlackChessPiece();
} else if ("white".equalsIgnoreCase(color)) {
piece = new WhiteChessPiece();
}
if (piece != null) {
pool.put(color, piece);
}
}
}
}
return piece;
}
/**
* 使用computeIfAbsent(推荐)
*/
public static ChessPiece getChessPieceV2(String color) {
return pool.computeIfAbsent(color, key -> {
if ("black".equalsIgnoreCase(key)) {
return new BlackChessPiece();
} else if ("white".equalsIgnoreCase(key)) {
return new WhiteChessPiece();
}
return null;
});
}
}
7.3 设置对象池上限
java
/**
* 带容量限制的享元工厂
*/
class BoundedFlyweightFactory {
private static final int MAX_POOL_SIZE = 100;
private static final Map<String, ChessPiece> pool =
new java.util.LinkedHashMap<String, ChessPiece>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, ChessPiece> eldest) {
// 超过最大容量时,移除最老的条目
return size() > MAX_POOL_SIZE;
}
};
public static synchronized ChessPiece getChessPiece(String color) {
return pool.computeIfAbsent(color, key -> {
System.out.println("创建新的享元对象: " + key);
// 创建逻辑...
return null;
});
}
}
7.4 享元对象的序列化
java
import java.io.Serializable;
/**
* 支持序列化的享元对象
*/
class SerializableFlyweight implements Serializable, ChessPiece {
private static final long serialVersionUID = 1L;
private final String color;
public SerializableFlyweight(String color) {
this.color = color;
}
@Override
public void place(int x, int y) {
System.out.println("放置" + color + "棋子在(" + x + ", " + y + ")");
}
/**
* 反序列化时返回池中的对象,而不是创建新对象
*/
private Object readResolve() {
return ChessPieceFactory.getChessPiece(color);
}
}
八、总结
8.1 核心要点
- 享元模式的本质:通过共享技术复用大量细粒度对象
- 关键概念 :
- 内部状态:可共享,不随环境变化
- 外部状态:不可共享,由客户端传入
- 享元工厂:管理和创建享元对象
- 适用场景 :
- 系统中存在大量相似对象
- 对象可以分为内部和外部状态
- 内存资源有限
- 核心优势 :
- 大幅减少对象数量
- 降低内存占用
- 提高系统性能
8.2 使用建议
选择享元模式的检查清单:
✓ 系统中是否有大量相似对象(>100个)?
✓ 对象是否可以分为内部状态和外部状态?
✓ 内部状态是否可以共享?
✓ 对象创建成本是否较高?
✓ 内存资源是否有限?
如果有3个以上"是",建议使用享元模式!
8.3 与其他模式的对比
享元 vs 单例:
- 享元:可以有多个共享对象(如黑子、白子)
- 单例:只有一个实例
享元 vs 对象池:
- 享元:强调对象共享和复用
- 对象池:强调对象的获取和释放
享元 vs 原型:
- 享元:共享同一个对象
- 原型:克隆出新对象
8.4 实践经验
- 合理划分状态:正确识别内部状态和外部状态
- 使用工厂管理:通过工厂模式管理享元对象池
- 注意线程安全:共享对象需要考虑并发访问
- 设置容量上限:避免对象池无限增长
- 性能监控:监控对象池的命中率和内存占用