对象池模式:池化管理、借出与归还、线程安全
对象池模式,说白了就是「复用对象,别老 new」。我刚开始做 Java 后端时,总觉得 new 一个对象能有多大事?直到有一次线上服务频繁 Full GC,一查发现是短生命周期对象创建太猛了。嗯,从那以后我对对象池就格外上心。
对象池的核心思想很简单:把创建好的对象放在一个池子里,谁用谁借,用完归还。这样避免了反复创建和销毁的开销。你想想看,数据库连接、线程、Socket 这些重量级对象,如果每次用都 new 一个,系统迟早扛不住。
对象池的三大核心操作
对象池模式主要围绕三个动作展开:
- 初始化:预先创建一批对象放进池子
- 借出:从池子里取一个可用对象,标记为「已占用」
- 归还:用完后放回池子,标记为「空闲」
我在项目中遇到过这样一个场景:一个消息推送服务,每次推送都要创建 TCP 连接。高峰期每秒上千次推送,连接创建销毁的开销直接让 CPU 飙到 90%。后来改用对象池管理连接,CPU 直接降到 30%。
线程安全:池子的命门
对象池在多线程环境下必须保证线程安全。否则两个线程同时借出同一个对象,那就乱套了。
常见的做法有几种:
- 使用
synchronized加锁 - 使用
ReentrantLock配合 Condition - 使用
Semaphore控制并发数量 - 使用
ConcurrentLinkedQueue等无锁队列
我个人习惯用 Semaphore + LinkedList 的组合。Semaphore 控制并发上限,LinkedList 做对象存储。这样既简单又高效。
ArrayList 做存储,结果并发环境下出现了数组越界。后来换成 ConcurrentLinkedQueue 才解决。记住,池子的存储结构必须线程安全。
一个简单的对象池实现
下面我用 Java 写一个通用的对象池框架。你直接拿去用,改改就能适配你的业务。
public class ObjectPool<T> {
private final List<T> pool = new LinkedList<>();
private final Semaphore semaphore;
private final ObjectFactory<T> factory;
public ObjectPool(int maxSize, ObjectFactory<T> factory) {
this.semaphore = new Semaphore(maxSize);
this.factory = factory;
// 预创建对象
for (int i = 0; i < maxSize; i++) {
pool.add(factory.create());
}
}
public T borrow() throws InterruptedException {
semaphore.acquire(); // 没资源就阻塞
synchronized (pool) {
return pool.remove(0);
}
}
public void release(T obj) {
synchronized (pool) {
pool.add(obj);
}
semaphore.release();
}
public interface ObjectFactory<T> {
T create();
}
}
这个实现有几个关键点:
- Semaphore 保证不会超出最大对象数
- synchronized 保证 pool 的并发安全
- 预创建对象避免了首次使用的延迟
线程池:对象池的经典应用
说到对象池,最典型的例子就是线程池。Java 的 ThreadPoolExecutor 本质上就是一个线程对象池。
线程池的核心参数:
| 参数 | 说明 | 我的建议 |
|---|---|---|
| corePoolSize | 核心线程数 | 根据 CPU 核心数 * 2 左右设置 |
| maxPoolSize | 最大线程数 | 不要超过 corePoolSize 的 2 倍 |
| keepAliveTime | 空闲线程存活时间 | 60 秒比较合理 |
| workQueue | 任务队列 | 用 LinkedBlockingQueue 或 ArrayBlockingQueue |
我记得有一次线上事故,就是因为线程池的 maxPoolSize 设置得太大,导致创建了上千个线程,直接把内存撑爆了。后来我把 maxPoolSize 控制在 200 以内,再配合合理的队列大小,系统就稳了。
对象复用的注意事项
对象复用不是万能的。有些场景下,复用反而会带来问题:
- 状态残留:对象用完后必须重置状态,否则下次使用会读到脏数据
- 连接有效性:数据库连接、Socket 连接可能因为网络原因断开,归还前要检查
- 内存泄漏:池中的对象如果持有外部引用,可能导致 GC 无法回收
pool.release(obj, obj::reset)。这样既灵活又安全。
对象池的适用场景
不是所有对象都需要池化。我一般遵循这个原则:
- 创建成本高(数据库连接、线程、大对象)→ 用池
- 创建成本低(小对象、无状态对象)→ 不用池
- 使用频率高 → 用池
- 使用频率低 → 不用池
你想想看,一个简单的 StringBuilder 对象,创建一次也就几纳秒,池化反而增加了管理开销,得不偿失。
对象池的监控与调优
对象池上线后,一定要加监控。我习惯加这几个指标:
- 当前池中对象总数
- 空闲对象数
- 借出对象数
- 等待借出的线程数
- 对象创建次数(如果池不够用,会动态创建)
通过这些指标,你可以判断池子大小是否合理。如果等待线程数长期大于 0,说明池子太小了,需要扩容。如果空闲对象数长期占满,说明池子太大了,浪费内存。