文章目录
- 一、背景
- 二、对象池有什么特征?
- 三、池的大小选择
- 四、运行原理
- 五、对象管理
- 5.1添加对象
- 5.2借用对象
- 5.3归还对象
- 5.4对象状态
- 六、对象池的使用
- 6.1 接入
- 6.2 实现线程池工厂
- 6.3 初始化
- 七、优缺点
- 八、应用场景
- 8.1Redis应用
- 8.2 Web服务器例子
- 8.3 游戏开发种的例子
- 九、总结
一、背景
减少频繁创建和销毁对象带来的成本,实现对象的缓存和复用,创建对象的成本比较大,并且创建比较频繁。比如线程的创建代价比较大,于是就有了常用的线程 池。对象池(模式)是一种创建型设计模式,它持有一个初始化好的对象的集合,将对象提供给调用者。
对象池模式是软件开发中广泛使用的设计模式,旨在通过重用创建成本高昂的对象来提高应用程序性能和效率。它在创建对象的新实例非常耗时且对象创建频率很高的情况下特别有用。当可以创建的对象实例数量由于资源限制而受到限制时,此模式也很有用。
一般而言对于 创建对象的成本比较大,并且创建比较频繁。比如线程的创建代价比较大,于是就有了常用的线程池。
二、对象池有什么特征?
一般来说,对象池有下面几个特征:
- 对象池中有一定数量已经创建好的对象
- 对象池向用户提供获取对象的接口,当用户需要新的对象时,便可通过调用此接口获取新的对象。如果对象池中有事先创建好的对象时,就直接返回给用 户;如果没有了,对象池还可以创建新的对象加入其中,然后返回给用户
- 对象池向用户提供归还对象的接口,当用户不再使用某对象时,便可通过此接口把该对象归还给对象池
三、池的大小选择
通常情况下,我们需要控制对象池的大小如果对象池没有限制,可能导致对象池持有过多的闲置对象,增加内存的占用。如果对象池闲置过小,没有可用的对象时,会造成之前对象池无可用的对象时,再次请求出现的问题。
对象池的大小选取应该结合具体的使用场景,结合数据(触发池中无可用对象的频率)分析来确定。现在Java的对象分配操作不比c语言的malloc调用慢, 对于轻中量级的对象, 分配/释放对象的开销可以忽略不计,并发环境中, 多个线程可能(同时)需要获取池中对象, 进而需要在堆数据结构上进行同步或者因为锁竞争而产生阻塞, 这种开销要比创建销毁对象的开销高数百倍;由于池中对象的数量有限, 势必成为一个可伸缩性瓶颈;很难正确的设定对象池的大小, 如果太小则起不到作用, 如果过大, 则占用内存资源高。
空间换时间的折中,本质上,对象池属于空间换时间的折中。它通过缓存初始化好的对象来提升调用者请求对象的响应速度。除此之外,折中(tradeoff)是软件开发中的一个重要的概念,会贯穿整个软件开发过程中。
四、运行原理
通过对象池获取对象,可能是通过工厂新创建的,也可能是空闲的对象;当对象获取成功且使用完成后,需要归还对象;在案例执行过程中,不断查询对象池中空闲和活跃对象的数量,用来监控池的变化。
五、对象管理
5.1添加对象
创建一个新对象并且放入池中,通常应用在需要预加载的场景中;涉及到两个核心操作:工厂创建对象,对象池化管理;
public void GenericObjectPool.addObject() throws Exception ;
5.2借用对象
public T GenericObjectPool.borrowObject(final long borrowMaxWaitMillis) throws Exception ;
首先从队列中获取对象;如果没有获取到,调用工厂创建方法,之后池化管理;对象获取之后会改变状态为ALLOCATED使用中;最后经过工厂的确认,完成对象获取动作;
5.3归还对象
public void GenericObjectPool.returnObject(final T obj) ;
归还对象的时候,首先转换为池化对象和标记RETURNING状态;经过多次校验判断,如果失败则销毁该对象,并重新维护对象池中可用的空闲对象;最终对象被标记为空闲状态,如果不超出最大空闲数,则对象被放到队列的某一端;
5.4对象状态
关于池化对象的状态在PooledObjectState类中有枚举和描述,在图中只是对部分几个状态流转做示意,更多细节可以参考状态类;
可以参考在上述案例中使用到的DefaultPooledObject默认池化对象类中相关方法,结合状态枚举,可以理解不同状态之间的校验和转换。
六、对象池的使用
6.1 接入
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
6.2 实现线程池工厂
import com.scl.online.service.model.SxInferContext;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.PooledObjectFactory;
import org.apache.commons.pool2.impl.DefaultPooledObject;
/**
* 实现PooledObjectFactory
*
*/
public class InferContextPooledObjectFactory implements PooledObjectFactory<SxInferContext> {
@Override
public PooledObject<SxInferContext> makeObject() {
SxInferContext inferContext = new SxInferContext();
return new DefaultPooledObject<>(inferContext);
}
@Override
public void destroyObject(PooledObject<SxInferContext> pooledObject) {
}
@Override
public boolean validateObject(PooledObject<SxInferContext> pooledObject) {
return true;
}
@Override
public void activateObject(PooledObject<SxInferContext> pooledObject) {
pooledObject.getObject().initObject();
}
@Override
public void passivateObject(PooledObject<SxInferContext> pooledObject) {
// 当ObjectPool实例返还池中的时候调用
pooledObject.getObject().initObject();
}
}
说明:
- SxInferContext:为对象池里头的对象,对象借还都会调用到PooledObjectFactory里头的方法
- PooledObjectFactory负责管理PooledObject,如:借出对象,返回对象,校验对象,有多少激活对象,有多少空闲对象。
方法 | 描述 |
---|---|
makeObject | 用于生成一个新的ObjectPool实例 |
activateObject | 每一个钝化(passivated)的ObjectPool实例从池中借出(borrowed)前调用 |
validateObject | 可能用于从池中借出对象时,对处于激活(activated)状态的ObjectPool实例进行测试确保它是有效的。也有可能在ObjectPool实例返还池中进行钝化前调用进行测试是否有效。它只对处于激活状态的实例调用 |
passivateObject | 当ObjectPool实例返还池中的时候调用 |
destroyObject | 当ObjectPool实例从池中被清理出去丢弃的时候调用(是否根据validateObject的测试结果由具体的实现在而定) |
6.3 初始化
public GenericObjectPool<SxInferContext> contextPools;
@PostConstruct
public void init() {
if (sxInferConfig.isObjectPoolUsable()) {
InferContextPooledObjectFactory factory = new InferContextPooledObjectFactory();
//设置对象池的相关参数
GenericObjectPoolConfig poolConfig = initConfig();
//新建一个对象池,传入对象工厂和配置
contextPools = new GenericObjectPool<>(factory, poolConfig);
}
}
/**
\* 池子初始化
*
\* @param
*/
public GenericObjectPoolConfig initConfig() {
GenericObjectPoolConfig cfg = new GenericObjectPoolConfig();
cfg.setJmxNamePrefix("objectPool");
// 对象总数
cfg.setMaxTotal(sxInferConfig.getPoolMaxTotal());
// 最大空闲对象数
cfg.setMaxIdle(sxInferConfig.getPoolMaxIdle());
// 最小空闲对象数
cfg.setMinIdle(sxInferConfig.getPoolMinIdle());
// 借对象阻塞最大等待时间
// 获取资源的等待时间。blockWhenExhausted 为 true 时有效。-1 代表无时间限制,一直阻塞直到有可用的资源
cfg.setMaxWaitMillis(sxInferConfig.getPoolMaxWait());
// 最小驱逐空闲时间
cfg.setMinEvictableIdleTimeMillis(sxInferConfig.getPoolMinEvictableIdleTimeMillis());
// 每次驱逐数量 资源回收线程执行一次回收操作,回收资源的数量。默认 3
cfg.setNumTestsPerEvictionRun(sxInferConfig.getPoolNumTestsPerEvictionRun());
// 回收资源线程的执行周期,默认 -1 表示不启用回收资源线程
cfg.setTimeBetweenEvictionRunsMillis(sxInferConfig.getPoolTimeBetweenEvictionRunsMillis());
// 资源耗尽时,是否阻塞等待获取资源,默认 true
cfg.setBlockWhenExhausted(sxInferConfig.isPoolBlockWhenExhausted());
return cfg;
}
说明:cfg.setJmxNamePrefix(“objectPool”); 假如项目中有用到redis线程池,则需要配置一下JmxNamePrefix。redis线程池使用的是“pool”,假如有重复的,早调用线程池是时,就默认会调用到Redis线程池的PooledObjectFactory(假如redis线程池使用默认的话),导致配置的线程池不生效。
GenericObjectPool 方法解释:
GenericObjectPool 方法解释:
方法 | 描述 |
---|---|
borrowObject | 从池中借出一个对象。要么调用PooledObjectFactory.makeObject方法创建,要么对一个空闲对象使用PooledObjectFactory.activeObject进行激活,然后使用PooledObjectFactory.validateObject方法进行验证后再返回 |
returnObject | 将一个对象返还给池。根据约定:对象必须 是使用borrowObject方法从池中借出的 |
invalidateObject | 废弃一个对象。根据约定:对象必须 是使用borrowObject方法从池中借出的。通常在对象发生了异常或其他问题时使用此方法废弃它 |
addObject | 使用工厂创建一个对象,钝化并且将它放入空闲对象池 |
getNumberIdle | |
getNumActive | 返回从借出的对象数量。如果这个信息不可用,返回一个负数 |
clear | 清除池中的所有空闲对象,释放其关联的资源(可选)。清除空闲对象必须使用PooledObjectFactory.destroyObject方法 |
close | 关闭池并释放关联的资源 |
七、优缺点
-
对象池优点
- 提高性能,对象池模式可以通过减少与对象创建和销毁相关的开销来显着提高应用程序的性能。通过重用预先初始化的对象,该模式减少了需要创建的对象数量,进而减少了创建新对象所需的时间和资源。
- 资源管理,对象池模式提供了一种管理共享资源的机制,例如数据库连接或文件句柄。通过限制创建的对象数量,该模式可以防止资源耗尽并确保资源得到有效共享。
- 一致性,对象池模式可以通过确保所有对象在使用前都预先初始化为已知状态来帮助确保应用程序的一致性。这在对象初始化复杂或耗时的情况下特别有用。
- 易于实现,对象池模式相对容易实现,可用于多种情况。它是一种经过验证的设计模式,已在许多应用程序和编程语言中成功使用。
-
对象池缺点
- 增加复杂性,对象池模式可以通过添加额外的抽象层来增加应用程序的复杂性。这会使代码更难理解和维护,尤其是在池大小和对象生命周期管理不当的情况下。
- 开销,虽然对象池模式可以通过减少与对象创建和销毁相关的开销来提高性能,但由于池本身的管理,它也会引入额外的开销。如果池大小没有针对应用程序的需要进行优化,这种开销会变得很大。
- 有限的灵活性:对象池模式旨在管理一组固定的对象,可能不适合需要动态对象创建或可变池大小的应用程序。
- 线程安全,如果多个线程同时访问池,对象池模式会引入线程安全问题。同步机制必须到位以确保一次只有一个线程可以访问池,这可能会增加额外的开销和代码的复杂性。
- 资源泄漏,如果对象没有正确返回到池中,它们可能会“泄漏”并且无法重用。随着时间的推移,这会导致资源耗尽并降低应用程序性能。
八、应用场景
8.1Redis应用
Lettuce作为Redis高级的客户端组件,通信层使用Netty组件,并且是线程安全,支持同步和异步模式,支持集群和哨兵模式;作为当下项目中常用的配置,其底层对象池基于common-pool2组件。
8.2 Web服务器例子
Web 服务器通常需要处理大量并发请求,这会给系统资源带来巨大压力。通过使用对象池来管理数据库连接、网络套接字或其他资源,从而提高Web 服务器的性能和可扩展性,避免资源耗尽。
8.3 游戏开发种的例子
游戏通常需要快速创建和销毁大量对象,例如粒子、子弹或敌人。通过使用对象池来管理这些对象,游戏可以提高性能并减少与对象创建和销毁相关的开销。
九、总结
对象池模式是一种强大的设计模式,可以通过重用昂贵的对象显著提高应用程序性能和效率。它提供了一种管理共享资源的机制,并通过限制创建的对象数量来防止资源耗尽。如果使用得当,对象池模式可以成为提高软件应用程序的可伸缩性和可靠性的有效工具。
本文从对象池的一个简单案例切入,主要分析common-pool2组件关于:池、工厂、配置、对象管理几个角色的源码逻辑,并且参考其在Redis中的实践,只是冰山一角,像这种通用型并且应用范围广的组件,很值得时常去读一读源码,真的令人惊叹其鬼斧天工的设计。