深入研究池化技术——对象池
池化技术
在优化系统性能方面发挥重要作用的技术手段被称为pool化技术。其核心理念在于将资源存储在一个集合中,在需要获取资源时从该集合中获取所需组件,并及时将它们放回资源集合中以避免浪费。这种做法能够减少资源分配和回收过程中的开销并提升整体效率特别是在实际开发实践中我们其实都在不自觉地运用pool化技术。例如缓存机制就是一个典型的pool化应用形式。关于pool化技术主要有集中式管理方案分布式管理方案以及混合型策略等不同的实现方式
- 对象池:通过复用对象实例的方式,在内存中实现资源的有效共享与复用,在一定程度上减少了对象实例的数量以及内存资源的浪费。
- 线程池:采用多线程复用来最大化资源利用率,在执行高强度任务时能够显著提升系统运行效率。
- 连接池:例如数据库连接池、Redis连接池以及HTTP相关应用中的默认数据库链接管理方式等,在这种模式下通过对TCP协议下重复利用TCP连接的方式减少了创建与释放的时间成本。
本文,我们详细来探讨一下对象池。
对象池
对象池负责管理一批规模较大的对象实例,并在对象生成耗时较长的情况下实现对运行效率的提升。该数据结构存在一定的学习门槛,并且在一定程度上增加了代码的复杂性
Commons-Pool2
Commons-Pool2属于Apache基金会的开放源代码对象 pool框架
官网地址
GitHub地址
Commons提供了两类对象池:
- ObjectPool
- KeyedObjectPool
ObjectPool
| 实现类 | 作用 |
|---|---|
| BaseObjectPool | 抽象类,用来扩展自己的对象池 |
| ErodingObjectPool | “腐蚀”对象池,代理一个对象池,并基于factor参数,为其添加“腐蚀”行为。归还的对象被腐蚀后,将会丢弃,而不是添加到空闲容量中。 |
| GenericObjectPool | 一个可配置的通用对象池实现。 |
| ProxiedObjectPool | 代理一个其他的对象池,并基于动态代理(支持JDK代理和CGLib代理),返回一个代理后的对象。该对象池主要用来增强对池化对象的控制,比如防止在归还该对象后,还继续使用该对象等。 |
| SoftReferenceObjectPool | 基于软引用的对象池 |
| SynchronizedObjectPool | 代理一个其他对象池,并为其提供线程安全的能力。 |
ObjectPool核心API
| 方法名 | 作用 |
|---|---|
| borrowObject | 从对象池中借对象 |
| returnObject() | 将对象归还到对象池 |
| invalidateObject() | 失效一个对象 |
| addObject() | 增加一个空闲对象,该方法适用于使用空闲对象预加载对象池 |
| clear() | 清空空闲的所有对象,并释放相关资源 |
| close() | 关闭对象池,并释放相关资源 |
| getNumIdle() | 获得空闲对象数量 |
| getNumActive() | 获得被借出对象数量 |
KeyedObjectPool
| 实现类 | 作用 |
|---|---|
| ErodingKeyedObjectPool | 作用 |
| GenericKeyedObjectPool | 类似GenericObjectPool |
| ProxiedKeyedObjectPool | 类似ProxiedObjectPool |
| SynchronizedKeyedObjectPool | 类似SynchronizedObjectPool |
它的核心差别在于它是基于Key来查找对象的;在设计层面来看,KeyedObjectPool与ObjectPool并无任何区别
快速入门
首先我们添加CommonsPools2的依赖:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.11.1</version>
</dependency>
定义一个对象,并模拟对象创建慢的场景:
public class SlowObject {
public SlowObject(String name, Integer age) {
this.name = name;
this.age = age;
}
public static SlowObject init() {
//模拟对象创建慢的场景
try {
Thread.sleep(10L);
}catch (InterruptedException e) {
e.printStackTrace();
}
return new SlowObject("小飞龙",20);
}
private String name;
private Integer age;
//getter and setter...
}
我们来使用GenericObjectPool创建对象池:

观察到他共有三个构造方法,在这些方法中使用一个参数的专门用于创建默认配置的对象池;此外还有两个参数以及三个参数的方法可接受外部配置输入。
BasedPooledObjectFactory 它是一个基于池化对象工厂进行扩展的抽象类,默认情况下会继承父类中的属性和方法
PoolUtils.SynchronizedPooledObjectFactory 是一个内部类角色,在软件架构中充当另一个PooledObjectFactory的代理角色,并负责实现线程同步机制的功能,在实际应用中通过调用PoolUtils.synchronizedPooledFactory()方法来创建实例对象
我们可以自定义一个实现类,来实现PooledObjectFactory:

可以看到,它需要实现很多的方法,基于这些方法,我做了一个总结:
| 方法名 | 作用 |
|---|---|
| makeObject | 创建一个对象实例,并将其包装成一个PooledObject |
| destroyObject | 销毁对象 |
| validateObject | 校验对象,确保对象池返回的对象都是OK的 |
| activateObject | 重新初始化对象 |
| passivateObject | 取消初始化对象。GenericObjectPool的addIdleObject、returnObject、evict调用该方法。 |
我们先来看一下makeObject方法:

可以看出该方法返回了一个 PooledObject。大家可能会疑问为什么不直接使用 Slow_OBJECT 而选择包装它呢?这也是 CommonsPool2 设计中的一个巧妙之处,在这种情况下 Pooled_OBJECT 能够包装当前使用的 Slow_OBJECT 实例。由于 Pooled_OBJECT 具有强大的功能特性目前已经有两个不同的实现类:
| 实现类 | 作用 |
|---|---|
| DefaultPooledObject | 包装原始对象,实现监控(例如创建时间、使用时间等)、状态跟踪等 |
| PooledSoftReference | 进一步封装了DefaultPooledObject,用来和SoftReferenceObjectPool配合使用。 |
值得一提的是,Commons-Pool2为PooledObject定义了若干种的状态:
| 状态 | 解释 |
|---|---|
| IDLE | 对象在队列中,并空闲 |
| ALLOCATED | 使用中(即出借中) |
| EVICTION_RETURN_TO_HEAD | 对象驱逐测试通过后,放回到队列头部 |
| VALIDATION | 对象当前在队列中,空闲校验中 |
| VALIDATION_PREALLOCATED | 对象不在队列中,出借前校验中 |
| VALIDATION_RETURN_TO_HEAD | 对象不在队列中,校验通过后放回头部 |
| INVALID | 对象失效,驱逐测试失败、校验失败、对象销毁,都会将对象置为INVALID |
| ABANDONED | 放逐中,如果对象上次使用时间超过removeAbandonedTimeout的配置,则将其标记为ABANDONED。标记为ABANDONED的对象即将变成INVALID |
| RETURNING | 对象归还池中 |
我们再继续编写代码,我们使用DefaultPoolObject就可以了:

同时,我们也要在构造方法里模拟对象创建慢的场景:

同时我们其他的方法我都打印一下日志,查看一下PooledObject的状态:
@Override
public void activateObject(PooledObject<SlowObject> pooledObject) throws Exception {
LOGGER.info("activateObject....state = {}",pooledObject.getState());
}
@Override
public void destroyObject(PooledObject<SlowObject> pooledObject) throws Exception {
LOGGER.info("destroyObject....state = {}",pooledObject.getState());
}
@Override
public PooledObject<SlowObject> makeObject() throws Exception {
DefaultPooledObject<SlowObject> object = new DefaultPooledObject<>(new SlowObject("小飞龙", 20));
LOGGER.info("makeObject....state = {}",object.getState());
return object;
}
@Override
public void passivateObject(PooledObject<SlowObject> pooledObject) throws Exception {
LOGGER.info("passivateObject....state = {}",pooledObject.getState());
}
@Override
public boolean validateObject(PooledObject<SlowObject> pooledObject) {
LOGGER.info("validateObject....state = {}",pooledObject.getState());
return true;
}
最后我们编写一个main方法:
public static void main(String[] args) throws Exception {
//创建对象池
GenericObjectPool<SlowObject> pool = new GenericObjectPool<>(new SlowObjectPooledObjectFactory());
//获取对象
SlowObject slowObject = pool.borrowObject();
//重新赋值
slowObject.setAge(21);
//再把对象归还对象池
pool.returnObject(slowObject);
}
运行一下试试看:

这样就可以看出对象的状态是怎样迁移的。
Commons-Pool2使用总结
Commons-Pool2还是比较简单的,它有以下几个组件:
-
ObjectPool:对象池子;其中最为关键的是:GenericObjectPool和GenericKeyedObjectPool。
- Factory:创建&管理PooledObject
一般要自己扩展
- Factory:创建&管理PooledObject
-
PooledObject:将现有对象纳入系统中以便实现对象池的管理;通常采用DefaultPooledObject作为默认配置。
Abandon与Evict区别与源码分析
在Commons-Pool2中,Abandon与Evict的主要区别是一个重点难点问题,并且经常出现在面试中。那么让我们先来探讨一下这个结论是什么。
*Abandon(废弃):ABANDONED标识符代表了一个特定的状态
一旦某个对象持续保持在ALDEL状态,则会被标记为ABANDONED标识符以表示其已被废弃。尽管该对象仍在当前的对象池中进行管理但并未被移除或清理。
- Evict:清理对象的过程
清理的不一定是ABANDONED对象
为了深入理解代码逻辑, 我们需要首先定位核心功能模块. 随后, 在BaseGenericObjectPool中定位该功能模块.

我们可以先到,它调用了一个startEvictor,翻译成中文,叫做启动清理器。

可以看出,在该系统中使用了一个名为EvictionTimer的时间控制组件。从组件名称就能看出这是一项定时任务。具体来说,在运行该组件之前系统会首先检查evictor变量是否存在(即其值是否为Null)。如果evictor变量不存在(即为Null),系统将创建一个新的evictor实例。随后会进一步检查设置的时间间隔delay参数是否为正值(即大于零)。如果是正值,则会调用EvictionTimer组件的时间调度方法进行操作。
而在这里设置的时间间隔delay的值由setTimeBetweenEvictionRuns方法传递过来。接下来的一行代码主要负责检查该值是否为空,并根据情况提供一个默认值以避免潜在的问题。

我们跟踪进入EvictionTimer:

通过查看由ScheduledThreadPoolExecutor基于构建的定时作业系统内部的工作流程是什么样的

看到在这个方法里负责处理定时作业的是一个线程池。其中主要负责的是一个称为Evictor的对象。我们将其纳入到内部空间中进行管理。

可以看到它是实现了Runnable,那核心方法肯定就是run方法了:
public void run() {
ClassLoader savedClassLoader = Thread.currentThread().getContextClassLoader();
try {
//校验ClassLoader
if (BaseGenericObjectPool.this.factoryClassLoader != null) {
ClassLoader cl = (ClassLoader)BaseGenericObjectPool.this.factoryClassLoader.get();
if (cl == null) {
this.cancel();
return;
}
Thread.currentThread().setContextClassLoader(cl);
}
try {
BaseGenericObjectPool.this.evict();
} catch (Exception var9) {
BaseGenericObjectPool.this.swallowException(var9);
} catch (OutOfMemoryError var10) {
var10.printStackTrace(System.err);
}
try {
BaseGenericObjectPool.this.ensureMinIdle();
} catch (Exception var8) {
BaseGenericObjectPool.this.swallowException(var8);
}
} finally {
Thread.currentThread().setContextClassLoader(savedClassLoader);
}
}
从结果来看,这段代码可以分成两部分来分析。首先进行的是对ClassLoader的检查,并不属于核心业务功能。接着分析第二部分:

这里调用了一个evict方法,我们看一下这个方法是怎么实现的:

可以看到,这段代码特别长,读起来也比较费事,大家不妨打断点通过debug的方式读一下,我们先来看一下第一行assertOpen():

它是用来判断对象池有没有被关闭,如果对象池关闭则抛出异常。
紧接着:

它首先检查idleObjects是否为非空值;如果non-empty,则随后启动了一个while循环来比较变量i与m的关系;当i超过m时,则会跳出该循环

我们看一下这个m是啥:

private int getNumTests() {
int numTestsPerEvictionRun = this.getNumTestsPerEvictionRun();
return numTestsPerEvictionRun >= 0 ? Math.min(numTestsPerEvictionRun, this.idleObjects.size()) : (int)Math.ceil((double)this.idleObjects.size() / Math.abs((double)numTestsPerEvictionRun));
}
原来它是通过计算numTestsPerEvictionRun和idleObjects的数量来确定一个最小值?而numTestsPerEvictionRun表示每次清理时处理的对象数量?所以这里的getNumTests是指每一次清理时需要处理的对象数量?
再往下看:

这一段都是在校验,表示这些对象都不需要清理。
再之后:

这里调了一个evictionPolicy.evict,跟踪进去看看:
public boolean evict(EvictionConfig config, PooledObject<T> underTest, int idleCount) {
return config.getIdleSoftEvictDuration().compareTo(underTest.getIdleDuration()) < 0 && config.getMinIdle() < idleCount || config.getIdleEvictDuration().compareTo(underTest.getIdleDuration()) < 0;
}
可以看出该系统通过一系列相关的配置参数EvictionConfig来实现对对象清除逻辑的判断机制,在什么情况下需要清除对象则由该算法进行布尔值判定返回true/false来决定是否执行清空操作

对于检测到需要清除的对象而言,在源代码中将该对象从源代码中的对象池中移除。而当evict设置为false时,在此逻辑分支下还有其他处理:

该人员会对testWildIdle的配置进行核查;当该人员将配置设置为true时,则会触发验证流程以验证对象;如果验证流程确认对象处于无效状态,则会进行处理。

这里也会用destory销毁对象,否则的话只是把对象做了一个钝化。
最后:

它会根据Abandoned的配置清理掉Abandoned的对象。
经过源码分析后会发现Abandoned是一个对象状态;而清除对象的实际逻辑则是通过将EvictionTimer与池中的对象结合在一起,并由它们执行evict方法来实现。
