本文主要是介绍apache Common Pool2,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
转载:https://blog.csdn.net/y4x5m0nivsrjay3x92c/article/details/80016747
我们系统中一般都会存在很多可重用并长期使用的对象,比如线程、TCP 连接、数据库连接等。虽然我们可以简单的在使用这些对象时进行创建、使用结束后销毁,但初始化和销毁对象的操作会造成一些资源消耗。我们可以使用对象池将这些对象集中管理,减少对象初始化和销毁的次数以节约资源消耗。
顾名思义,对象池简单来说就是存放对象的池子,可以存放任何对象,并对这些对象进行管理。它的优点就是可以复用池中的对象,避免了分配内存和创建堆中对象的开销;避免了释放内存和销毁堆中对象的开销,进而减少垃圾收集器的负担;避免内存抖动,不必重复初始化对象状态。对于构造和销毁比较耗时的对象来说非常合适。
当然,我们可以自己去实现一个对象池,不过要实现的比较完善还是要花上不少精力的。所幸的是, Apache 提供了一个通用的对象池技术的实现: Common Pool2,可以很方便的实现自己需要的对象池。Jedis 的内部对象池就是基于 Common Pool2 实现的。
核心接口
Common Pool2 的核心部分比较简单,围绕着三个基础接口和相关的实现类来实现:
- ObjectPool:对象池,持有对象并提供取/还等方法。
- PooledObjectFactory:对象工厂,提供对象的创建、初始化、销毁等操作,由 Pool 调用。一般需要使用者自己实现这些操作。
- PooledObject:池化对象,对池中对象的封装,封装对象的状态和一些其他信息。(由对象工厂创建的对象就是池化对象)
Common Pool2 提供的最基本的实现就是由 Factory 创建对象并使用PooledObject 封装对象(池化对象)放入 Pool 中。
对象池实现
对象池有两个基础的接口 ObjectPool
和 KeyedObjectPool
, 持有的对象都是由 PooledObject
封装的池化对象。 KeyedObjectPool
的区别在于其是用键值对的方式维护对象。
ObjectPool
和 KeyedObjectPool
分别有一个默认的实现类GenericObjectPool
和 GenericKeyedObjectPool
可以直接使用,他们的公共部分和配置被抽取到了 BaseGenericObjectPool
中。
SoftReferenceObjectPool
是一个比较特殊的实现,在这个对象池实现中,每个对象都会被包装到一个SoftReference
中。SoftReference
允许垃圾回收机制在需要释放内存时回收对象池中的对象,可以避免一些内存泄露的问题。
对象池接口
下面简单介绍一下 ObjectPool
接口的核心方法,
public interface ObjectPool<T>
KeyedObjectPool
和ObjectPool
类似,区别在于方法多了个参数: K key
。
-
从池中获取一个对象:
T borrowObject() throws Exception, NoSuchElementException, IllegalStateException;
客户端在使用完对象后必须使用 returnObject 方法返还获取的对象 -
将对象返还到池中:
void returnObject(T obj) throws Exception;
对象必须是从 borrowObject 方法获取到的 -
使池中的对象失效:
void invalidateObject(T obj) throws Exception;
当获取到的对象被确定无效时(由于异常或其他问题),应该调用该方法 -
池中当前闲置的对象数量:
int getNumIdle(); -
当前从池中借出的对象的数量:
int getNumActive(); -
清除池中闲置的对象:
void clear() throws Exception, UnsupportedOperationException; -
关闭这个池,并释放与之相关的资源:
void close();
PooledObjectFactory
对象工厂,负责对象的创建、初始化、销毁和验证等工作。
Factory
对象由ObjectPool
持有并使用。
public interface PooledObjectFactory<T>
- 创建一个池对象 PooledObject makeObject() throws Exception;
- 销毁对象 void destroyObject(PooledObject p) throws Exception;
- 验证对象是否可用 boolean validateObject(PooledObject p);
- 激活对象,从池中取对象时会调用此方法 void activateObject(PooledObject p) throws
Exception; - 钝化对象,向池中返还对象时会调用此方法 void passivateObject(PooledObject p) throws
Exception;
Common Pool2
并没有提供 PooledObjectFactory
可以直接使用的子类实现,因为对象的创建、初始化、销毁和验证的工作无法通用化,需要由使用方自己实现。
不过它提供了一个抽象子类 BasePooledObjectFactory
,实现自己的工厂时可以继承BasePooledObjectFactory
,就只需要实现 create
和 wrap
两个方法了。
PooledObject
PooledObject
有两个实现类,DefaultPooledObject
是普通通用的实现,PooledSoftReference
使用 SoftReference 封装了对象,供SoftReferenceObjectPool
使用。
下面是 PooledObject
接口的一些核心方法:
public interface PooledObject<T> extends Comparable<PooledObject<T>>
- 获取封装的对象 T getObject();
- 对象创建的时间 long getCreateTime();
- 对象上次处于活动状态的时间 long getActiveTimeMillis();
- 对象上次处于空闲状态的时间 long getIdleTimeMillis();
- 对象上次被借出的时间 long getLastBorrowTime();
- 对象上次返还的时间 long getLastReturnTime();
- 对象上次使用的时间 long getLastUsedTime();
- 将状态置为 PooledObjectState.INVALID void invalidate();
- 更新 lastUseTime void use();
- 获取对象状态 PooledObjectState getState();
- 将状态置为 PooledObjectState.ABANDONED void markAbandoned();
- 将状态置为 PooledObjectState.RETURNING void markReturning();
// 创建一个新对象;当对象池中的对象个数不足时,将会使用此方法来"输出"一个新的"对象",并交付给对象池管理
PooledObject makeObject() throws Exception;//
销毁对象,如果对象池中检测到某个"对象"idle的时间超时,或者操作者向对象池"归还对象"时检测到"对象"已经无效,那么此时将会导致"对象销毁";
//
"销毁对象"的操作设计相差甚远,但是必须明确:当调用此方法时,"对象"的生命周期必须结束.如果object是线程,那么此时线程必须退出;
// 如果object是socket操作,那么此时socket必须关闭;如果object是文件流操作,那么此时"数据flush"且正常关闭.
void destroyObject(PooledObject p) throws Exception;//
检测对象是否"有效";Pool中不能保存无效的"对象",因此"后台检测线程"会周期性的检测Pool中"对象"的有效性,如果对象无效则会导致此对象从Pool中移除,并destroy;
// 此外在调用者从Pool获取一个"对象"时,也会检测"对象"的有效性,确保不能讲"无效"的对象输出给调用者; //
当调用者使用完毕将"对象归还"到Pool时,仍然会检测对象的有效性.所谓有效性,就是此"对象"的状态是否符合预期,是否可以对调用者直接使用;
// 如果对象是Socket,那么它的有效性就是socket的通道是否畅通/阻塞是否超时等. boolean
validateObject(PooledObject p);
// “激活"对象,当Pool中决定移除一个对象交付给调用者时额外的"激活"操作,比如可以在activateObject方法中"重置"参数列表让调用者使用时感觉像一个"新创建"的对象一样;如果object是一个线程,可以在"激活"操作中重置"线程中断标记”,或者让线程从阻塞中唤醒等;
// 如果object是一个socket,那么可以在"激活操作"中刷新通道,或者对socket进行链接重建(假如socket意外关闭)等.
void activateObject(PooledObject p) throws Exception;
// “钝化"对象,当调用者"归还对象"时,Pool将会"钝化对象”;钝化的言外之意,就是此"对象"暂且需要"休息"一下.
// 如果object是一个socket,那么可以passivateObject中清除buffer,将socket阻塞;如果object是一个线程,可以在"钝化"操作中将线程sleep或者将线程中的某个对象wait.需要注意的时,activateObject和passivateObject两个方法需要对应,避免死锁或者"对象"状态的混乱.
void passivateObject(PooledObject p) throws Exception;
对象池配置
对象池配置提供了对象池初始化所需要的参数,Common Pool2 中的基础配置类是BaseObjectPoolConfig
。其有两个实现类分别为 GenericObjectPoolConfig
和 GenericKeyedObjectPoolConfig
,分别为 GenericObjectPool
和GenericKeyedObjectPool
所使用。
下面是一些重要的配置项:(基本 commons-pool2-2.3.jar)
-
maxTotal 允许创建对象的最大数量,默认值 8,-1 代表无数量限制(int类型)
-
maxIdle 最大空闲资源数,默认值 8
-
minIdle 最小空闲资源数,默认值 0,软标准情况下,至少留下几个object继续保持idle(节省开销),软标准,结合idleSoftEvictTime和timeBetweenEvictionRunsMillis使用
-
lifo 对象池存储空闲对象是使用的LinkedBlockingDeque,它本质上是一个支持FIFO和FILO的双向的队列,common-pool2中的LinkedBlockingDeque不是Java原生的队列,而有common-pool2重新写的一个双向队列。如果为true,表示使用FIFO获取对象(资源的存取数据结构,默认值 true,true 资源按照栈结构存取,false 资源按照队列结构存取)
-
fairness 从池中获取/返还对象时是否使用公平锁机制,默认为 false
-
maxWaitMillis 获取对象时的等待时间,单位毫秒。当 blockWhenExhausted 配置为 true 时,此值有效。 -1 代表无时间限制,一直阻塞直到有可用的资源。(long类型)
-
minEvictableIdleTimeMillis 对象空闲的最小时间,达到此值后空闲对象将可能会被移除。-1 表示不移除;默认 30 分钟,在资源回收策略中,会使用到
-
softMinEvictableIdleTimeMillis 同上,额外的条件是池中至少保留有 minIdle 所指定的个数的对象在资源回收策略中,会使用到
-
numTestsPerEvictionRun 资源回收线程执行一次回收操作,回收资源的数量。默认 3
当 设置为0时,不回收资源。 设置为 小于0时,回收资源的个数为 (int)Math.ceil( 池中空闲资源个数 / Math.abs(numTestsPerEvictionRun) );
设置为 大于0时,回收资源的个数为 Math.min( numTestsPerEvictionRun,池中空闲的资源个数 ); -
testOnCreate 创建对象时是否调用 factory.validateObject 方法,默认 false
-
testOnBorrow 取对象时是否调用 factory.validateObject 方法,默认 false
-
testOnReturn 返还对象时是否调用 factory.validateObject 方法,默认 false
当将资源返还个资源池时候,验证资源的有效性,调用 factory.validateObject()方法,如果无效,则调用 factory.destroyObject()方法 -
testWhileIdle 池中的闲置对象是否由逐出器验证。无法验证的对象将从池中删除销毁。默认 false
-
timeBetweenEvictionRunsMillis 回收资源线程的执行周期,默认 -1 表示不启用回收资源线程,即不开启驱逐线程,所以与之相关的参数是没有作用的
-
minEvictableIdleTimeMillis 最小的驱逐时间,对象最小的空闲时间。如果为小于等于0,最Long的最大值,如果大于0,当空闲的时间大于这个值时,执行移除这个对象操作。默认值是1000L * 60L * 30L;即30分钟。可以避免(连接)泄漏。
-
SoftMinEvictableIdleTimeMillis 也是最小的驱逐时间,对象最小的空间时间,如果小于等于0,取Long的最大值,如果大于0,当对象的空闲时间超过这个值,并且当前空闲对象的数量大于最小空闲数量(minIdle)时,执行移除操作。这个和上面的minEvictableIdleTimeMillis的区别是,它会保留最小的空闲对象数量。而上面的不会,是强制性移除的。默认值是-1,通常该值设置的比minEvictableIdleTimeMillis要小;
-
evictionPolicyClassName 资源回收策略,默认值
org.apache.commons.pool2.impl.DefaultEvictionPolicy
驱逐线程使用的策略类名,之前的minEvictableIdleTimeMillis和softMinEvictableIdleTimeMillis就是默认策略DefaultEvictionPolicy的实现,这个两个参数,在资源回收策略中,会使用到 -
blockWhenExhausted 资源耗尽时,是否阻塞等待获取资源,默认 true
-
参数whenExhaustedAction指定在池中借出对象的数目已达极限的情况下,调用它的borrowObject方法时的行为。可以选用的值有:
GenericObjectPool.WHEN_EXHAUSTED_BLOCK,表示等待;
GenericObjectPool.WHEN_EXHAUSTED_GROW,表示创建新的实例(不过这就使maxActive参数失去了意义);
GenericObjectPool.WHEN_EXHAUSTED_FAIL,表示抛出一个java.util.NoSuchElementException异常。
我们要思考下,这样对理解上面的参数很有用
- 池有多大,如果请求超过池大小,客户端是等待(如果等待,等待多少时间),还是立即返回失败
- 池中对象,如果空闲了,不管它,还是开避线程定时清理(多少时间清理一次,一次清理多少个)
池化对象的状态
池化对象的状态定义在 PooledObjectState 枚举中,有以下值:
- IDLE 在池中,处于空闲状态
- ALLOCATED 被使用中
- EVICTION 正在被逐出器验证
- VALIDATION 正在验证
- INVALID 驱逐测试或验证失败并将被销毁
- ABANDONED 对象被客户端拿出后,长时间未返回池中,或没有调用 use 方法,即被标记为抛弃的
这些状态的转换逻辑大致如下图:
PooledObjectState类枚举的PooledObject状态及其转换关系
状态 | 描述 |
---|---|
IDLE | 位于队列中,未使用 |
ALLOCATED | 在使用 |
EVICTION | 位于队列中,当前正在测试,可能会被回收 |
EVICTION_RETURN_TO_HEAD | 不在队列中,当前正在测试,可能会被回收。如果客户端试图借出该正在被测试的对象,需等到测试完毕后才能借出并且从队列中移除;待测试完毕后,如果没被回收该对象会放置在队列的开头位置。 |
VALIDATION | 位于队列中,当前正在验证 |
VALIDATION_PREALLOCATED | 不在队列中,当前正在验证。当对象从池中被借出。在配置了testOnBorrow的情况下,对像从队列移除和进行预分配的时候会进行验证 |
VALIDATION_RETURN_TO_HEAD | 不在队列中,正在进行验证。从池中借出对象时, |
从队列移除对象时会先进行测试。返回到队列头部的时候应该做一次完整的验证 | |
INVALID | 回收或验证失败,将销毁 |
ABANDONED | 即将无效 |
RETURNING | 返还到池中 |
网友遇到的问题1:
mysql服务端设置了连接8小时失效,但是commons-pool2对应的对象池中没有配置上timeBetweenEvictionRunsMillis minEvictableIdleTimeMillis numTestsPerEvictionRun,导致没有对池化的mysql客户端进行检测,所以经验是服务器端如果设置了idel>0的空闲时间, 那么客户端最好设置上对应的心跳频率即多久心跳一次;
网友遇到的问题2:
redis的服务端设置了timeout=0,由于网络原因,commons-pool2已经将池中redis客户端销毁,但是服务端redis因为配置了timeout=0禁用了关闭限制的redis客户端功能,导致服务端大量僵尸进程存在,所以经验是配置redis服务端的timeout为一个大于0的值,意思是客户端如果空闲了且空闲时间大于该值,服务端就会关闭该连接
从池中获取资源的逻辑
1: 如果 blockWhenExhausted 配置的 为 false,从资源池中获取资源,如果获取不到,则判断当前池中的对象数量是否超过了 maxTotal 设置的数量,如果没有超过, 则通过调用factory.makeObject() 创建对象,并将对象放入池中,执行第2步 。如果超过了,则返回 null,逻辑到此结束。
如果 blockWhenExhausted 配置的 为 true ,从资源池中获取资源,如果获取不到,则判断当前池中的对象数量是否超过了 maxTotal 设置的数量,如果没有超过,
则通过调用factory.makeObject() 创建对象,并将对象放入池中,执行第2步 。如果超过了,则阻塞等待,如果 MaxWaitMillis 配置的为 -1 则 阻塞等待,直到有可用的资源为止。
如果 maxWaitMillis 配置为 1000 则 阻塞等待 1000毫秒,如果有可用资源,执行第2步,如果没有则返回 null,逻辑到此结束。
2:将资源的状态 修改为 已分配,执行 第 3 步
3:调用 factory.activateObject() 方法,执行 第 4步
4:如果 testOnBorrow 或者 testOnCreate 中有一个 配置 为 true 时,则调用 factory.validateObject() 方法
5:以上步骤都完成了,返回 资源对象
将资源返还给池的逻辑
1:检查配置参数 testOnReturn,如果 为 true,调用 factory.validateObject()方法,验证资源对象的有效性,验证结果为 false,则调用 factory.destroyObject()方法,逻辑到此结束。
验证结果为 true,则执行第 2 步。
2:调用 factory.passivateObject()方法,然后执行 第 3 步
3:将资源的状态 修改为 未分配,执行第 4 步
4:进行判断 ( 资源池是否关闭 || (maxIdle > -1 ) && ( maxIdle <= 资源池空闲资源个数) )
如果 判断为 true,则调用 factory.destroyObject()方法,逻辑到此结束。
如果 判断为 false,则 将资源返还给资源池,逻辑到此结束。
Apache_common_pool 启动一个线程执行释放资源的工作(使用 java.util.Timer 实现)从池中回收资源逻辑(回收资源的意思是,将资源从池中删除掉,例如,如果是TCP链接,则需要将链接断开,并从池中删除掉。)
1:根据 evictionPolicyClassName
配置的参数创建回收策略,
默认回收策略源码
import org.apache.commons.pool2.PooledObject;
public class DefaultEvictionPolicy
implements EvictionPolicy
{
public boolean evict(EvictionConfig config, PooledObject underTest, int idleCount)
{
if (((config.getIdleSoftEvictTime() < underTest.getIdleTimeMillis()) && (config.getMinIdle() < idleCount)) || (config.getIdleEvictTime() < underTest.getIdleTimeMillis()))
{
return true;
}
return false;
}
}
if条件判断与
(softMinEvictableIdleTimeMillis < 资源的空闲时间 && Math.min(maxIdle,minIdle) < 目前池中空闲的资源数) ||
minEvictableIdleTimeMillis < 资源的空闲时间
等价
2:根据配置的参数 numTestsPerEvictionRun 计算,要回收的资源数量(具体的计算规则,请参照源码)
3:根据回收策略判断,资源是否需要回收。如果 是 则将资源从池中删除,并调用factory.destroyObject()方法。
如果 否 则根据配置的 testWhileIdle 参数,判断 是否执行 factory.activateObject()和factory.validateObject() ,factory.passivateObject() 方法。如果 testWhileIdle 配置为 true,则以次执行 factory.activateObject(),factory.validateObject(),factory.passivateObject()
这篇关于apache Common Pool2的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!