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 中。

对象池实现
在这里插入图片描述
对象池有两个基础的接口 ObjectPoolKeyedObjectPool, 持有的对象都是由 PooledObject 封装的池化对象。 KeyedObjectPool 的区别在于其是用键值对的方式维护对象。

ObjectPoolKeyedObjectPool 分别有一个默认的实现类GenericObjectPoolGenericKeyedObjectPool 可以直接使用,他们的公共部分和配置被抽取到了 BaseGenericObjectPool 中。

SoftReferenceObjectPool 是一个比较特殊的实现,在这个对象池实现中,每个对象都会被包装到一个SoftReference中。SoftReference允许垃圾回收机制在需要释放内存时回收对象池中的对象,可以避免一些内存泄露的问题。

对象池接口
下面简单介绍一下 ObjectPool 接口的核心方法,

public interface ObjectPool<T> 

KeyedObjectPoolObjectPool 类似,区别在于方法多了个参数: 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,就只需要实现 createwrap 两个方法了。

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。其有两个实现类分别为 GenericObjectPoolConfigGenericKeyedObjectPoolConfig,分别为 GenericObjectPoolGenericKeyedObjectPool 所使用。

下面是一些重要的配置项:(基本 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()

猜你喜欢

转载自blog.csdn.net/weixin_42868638/article/details/87070582