享元模式详解:从原理到实战,看Java如何优雅节省内存

内容分享2小时前发布
0 1 0

开篇:你的系统是否遭遇这些痛点?

在互联网高并发场景下,你是否遇到过这样的问题:

创建大量类似对象导致内存暴涨?
频繁创建销毁对象引发GC频繁,系统卡顿?
数据库连接、线程资源消耗巨大?

今天,我们将深入探讨一个经典而强劲的设计模式——享元模式(Flyweight Pattern),看它如何在JDK源码、各大开源框架中大显身手,协助我们优雅地解决这些难题!

一、什么是享元模式?

通过共享技术有效支持大量细粒度对象的复用

享元模式将对象的状态分为两类:

内部状态(Intrinsic State)

存储在享元对象内部
不会随环境改变而改变
可以共享

外部状态(Extrinsic State)

随环境改变而改变
不可共享
由客户端保存并传入

适用场景

 ✅ 系统中存在大量类似对象
 ✅ 对象的大部分状态可以外部化
 ✅ 需要缓冲池的场景(连接池、线程池)
 ✅ 使用享元模式的额外开销能被节省的内存抵消

模式结构

享元模式包含四大角色:

Flyweight(抽象享元)
定义享元对象的接口
ConcreteFlyweight(具体享元)
实现抽象享元接口
UnsharedConcreteFlyweight(非共享享元)
不需要共享的享元子类
FlyweightFactory(享元工厂)
负责创建和管理享元对象

享元模式详解:从原理到实战,看Java如何优雅节省内存

享元模式系统架构

二、经典案例:五子棋游戏中的棋子管理

问题场景

想象一个五子棋游戏,棋盘有15×15=225个位置。如果为每个棋子都创建一个对象,内存消耗巨大。但实际上:

  • 所有黑棋的颜色、形状都一样,只有位置不同
  • 所有白棋的颜色、形状都一样,只有位置不同

关键洞察: 颜色、形状是内部状态(可共享),位置是外部状态(不可共享)

代码实现

/ 抽象享元:棋子接口
public interface ChessPiece {
    void display(int x, int y);
}
// 具体享元:具体棋子
public class ConcreteChessPiece implements ChessPiece {
    private String color; // 内部状态:颜色
    private String shape; // 内部状态:形状
    public ConcreteChessPiece(String color) {
        this.color = color;
        this.shape = "圆形";
        System.out.println("创建了一个" + color + "棋子对象");
    }
    @Override
    public void display(int x, int y) {
        System.out.println("在位置[" + x + "," + y + "]放置" + color + shape + "棋子");
    }
}
// 享元工厂
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) {
            piece = new ConcreteChessPiece(color);
            pool.put(color, piece);
        }
        return piece;
    }
    public static int getTotalPieces() {
        return pool.size();
    }
}

运行结果

创建了一个黑色棋子对象
在位置[1,1]放置黑色圆形棋子
创建了一个白色棋子对象
在位置[1,2]放置白色圆形棋子
在位置[2,1]放置黑色圆形棋子
在位置[2,2]放置白色圆形棋子
实际创建的棋子对象数量:2
black1 == black2: true

效果: 放置了4个棋子,实际只创建了2个对象!内存节省50%!

享元模式详解:从原理到实战,看Java如何优雅节省内存

五子棋游戏

三、JDK中的享元模式应用

3.1 String常量池

Java的String常量池是享元模式的典型应用!

public class StringPoolExample {
    public static void main(String[] args) {
        String s1 = "Hello";  // 字符串字面量,存储在常量池
        String s2 = "Hello";  // 复用常量池中的对象
        String s3 = "Hello";  // 复用常量池中的对象
        String s4 = new String("Hello");  // 新对象,不在常量池
        String s5 = s4.intern();  // 手动加入常量池
        System.out.println("s1 == s2: " + (s1 == s2)); // true
        System.out.println("s1 == s3: " + (s1 == s3)); // true
        System.out.println("s1 == s4: " + (s1 == s4)); // false
        System.out.println("s1 == s5: " + (s1 == s5)); // true
    }
}

原理: JVM自动将字符串字面量放入常量池,实现自动共享!

3.2 Integer缓存池

Java对基本类型包装类也应用了享元模式。Integer类对-128到127之间的整数进行了缓存。

public class IntegerCacheExample {
    public static void main(String[] args) {
        // 在缓存范围内(-128到127)
        Integer a1 = 100;
        Integer a2 = 100;
        System.out.println("a1 == a2: " + (a1 == a2)); // true
        // 超出缓存范围
        Integer b1 = 200;
        Integer b2 = 200;
        System.out.println("b1 == b2: " + (b1 == b2)); // false
    }
}

源码揭秘:Integer.valueOf()

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

享元模式详解:从原理到实战,看Java如何优雅节省内存

JDK中的享元模式

四、生产级应用:数据库连接池

为什么需要连接池?

数据库连接是一种重量级资源,创建和销毁连接的开销超级大:

  • TCP三次握手建立连接:耗时10-50ms
  • 数据库认证过程:耗时5-20ms
  • 资源分配(内存、文件描述符等)

⚠️ 痛点: 如果每次数据库操作都创建新连接,在高并发场景下系统性能将严重下降!

简化版连接池实现

/ 数据库连接(享元对象)
public class DatabaseConnection {
    private String connectionId;
    private boolean inUse;
    public DatabaseConnection(String id) {
        this.connectionId = id;
        this.inUse = false;
        // 模拟创建连接的耗时操作
        try {
            Thread.sleep(100);
            System.out.println("创建数据库连接:" + id);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public void executeQuery(String sql) {
        System.out.println("[" + connectionId + "] 执行SQL: " + sql);
    }
    // getter和setter省略...
}
// 连接池工厂(享元工厂)
public class ConnectionPool {
    private static final int POOL_SIZE = 5;
    private List<DatabaseConnection> connections = new ArrayList<>();
    public ConnectionPool() {
        // 初始化连接池
        for (int i = 0; i < POOL_SIZE; i++) {
            connections.add(new DatabaseConnection("CONN-" + (i + 1)));
        }
    }
    public synchronized DatabaseConnection getConnection() {
        for (DatabaseConnection conn : connections) {
            if (!conn.isInUse()) {
                conn.setInUse(true);
                System.out.println("从连接池获取连接:" + conn.getConnectionId());
                return conn;
            }
        }
        System.out.println("连接池已满,等待中...");
        return null;
    }
    public synchronized void releaseConnection(DatabaseConnection conn) {
        conn.setInUse(false);
        System.out.println("释放连接回连接池:" + conn.getConnectionId());
    }
}

性能对比:震撼的数据

指标

不使用连接池

使用连接池

性能提升

每次操作耗时

~150ms

~5ms

30倍

1000次操作总耗时

~150秒

~5秒

30倍

内存占用

不稳定(频繁GC)

稳定

减少70%

并发能力

提升80%

享元模式详解:从原理到实战,看Java如何优雅节省内存

数据库连接池性能对比

️ 五、开源框架中的享元模式

5.1 Apache Commons Pool

Apache Commons Pool是一个通用的对象池化框架,广泛应用于各种池化场景。

import org.apache.commons.pool2.impl.GenericObjectPool;
// 配置对象池
GenericObjectPoolConfig<ExpensiveObject> config = new GenericObjectPoolConfig<>();
config.setMaxTotal(5);  // 最大对象数
config.setMaxIdle(3);   // 最大空闲对象数
config.setMinIdle(1);   // 最小空闲对象数
// 创建对象池
GenericObjectPool<ExpensiveObject> pool = 
    new GenericObjectPool<>(new ExpensiveObjectFactory(), config);
// 使用对象池
ExpensiveObject obj = pool.borrowObject();
obj.doWork("任务-1");
pool.returnObject(obj);

5.2 线程池(ThreadPoolExecutor)

Java的线程池也是享元模式的典型应用,通过复用线程避免频繁创建销毁的开销。

// 创建固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(3);
// 提交10个任务,只会创建3个线程复用
for (int i = 0; i < 10; i++) {
    final int taskId = i;
    executor.submit(() -> {
        System.out.println("任务 " + taskId + 
            " 由线程 " + Thread.currentThread().getName() + " 执行");
    });
}

享元模式详解:从原理到实战,看Java如何优雅节省内存

线程池工作量原理

六、享元模式的优缺点

✅ 优点

1.大幅减少对象创建数量
,降低内存占用
2.提高系统性能
,避免频繁GC
3.提高对象复用率
,减少创建销毁开销
4.外部状态独立
,不会影响内部状态

❌ 缺点

1.增加系统复杂度,需要分离内部和外部状态
2.读取外部状态的开销,可能抵消部分性能提升
3.线程安全问题,共享对象需要思考并发访问
4.不适合状态常常变化的对象

七、最佳实践与注意事项

何时使用享元模式?

✔️ 必须满足:

  • 对象数量巨大(成百上千)
  • 内存压力大(频繁GC)
  • 对象可共享(大部分状态可外部化)
  • 创建开销大(耗时或消耗资源多)

实现要点

public class FlyweightBestPractice {
    // 1️⃣ 使用线程安全的容器
    private static final ConcurrentHashMap<String, Object> pool = 
        new ConcurrentHashMap<>();
    // 2️⃣ 使用双重检查锁确保线程安全
    public static Object getFlyweight(String key) {
        Object obj = pool.get(key);
        if (obj == null) {
            synchronized (pool) {
                obj = pool.get(key);
                if (obj == null) {
                    obj = createObject(key);
                    pool.put(key, obj);
                }
            }
        }
        return obj;
    }
    // 3️⃣ 设置池的大小上限,避免内存溢出
    private static final int MAX_POOL_SIZE = 100;
    // 4️⃣ 提供清理机制
    public static void clear() {
        pool.clear();
    }
}

与其他模式的协作

与工厂模式结合
:享元工厂负责创建和管理享元对象
与单例模式结合
:享元工厂一般设计为单例
与状态模式结合
:享元对象的状态变化可以用状态模式管理
与组合模式结合
:可以将享元对象组合成更复杂的结构

享元模式详解:从原理到实战,看Java如何优雅节省内存

和其他模式对比

八、总结

享元模式是一个强劲的性能优化工具,通过对象共享实现内存和性能的双重优化。在实际开发中,我们已经在使用它:

 JDK自带:
String常量池
包装类缓存池(Integer、Long等)
 数据库领域:
连接池(HikariCP、Druid)
 并发编程:
线程池(ThreadPoolExecutor)
 缓存框架:
Redis连接池、对象池

关键要点回顾

✨ 区分内部状态和外部状态
✨ 使用工厂管理享元对象
✨ 注意线程安全问题
✨ 在合适的场景使用,避免过度设计
© 版权声明

相关文章

1 条评论

  • 头像
    飞沙 读者

    收藏了,感谢分享

    无记录
    回复