利用commons-pool2自定义对象池
一、为什么使用对象池
恰当地使用对象池化技术,可以有效地减少对象生成和初始化时的消耗,提高系统的运行效率。commons-pool2是Apache下一个开源的公共资源池。我们可以根据它来快速的建立一个自己的对象池。组件提供了一整套用于实现对象池化的框架,以及若干种各具特色的对象池实现。官方网址:http://commons.apache.org/proper/commons-pool2/
二、对象池思路
对象池化的基本思路是:将用过的对象保存起来,等下一次需要这种对象的时候,再拿出来重复使用,从而在一定程度上减少频繁创建对象所造成的开销。用于充当保存对象的“容器”的对象,被称为“对象池”(Object Pool,或简称Pool)。
对于没有状态的对象(例如String),在重复使用之前,无需进行任何处理;对于有状态的对象(例如StringBuffer),在重复使用之前,就需要把它们恢复到等同于刚刚生成时的状态。由于条件的限制,恢复某个对象的状态的操作不可能实现了的话,就得把这个对象抛弃,改用新创建的实例了。
并非所有对象都适合拿来池化――因为维护对象池也要造成一定开销。对生成时开销不大的对象进行池化,反而可能会出现“维护对象池的开销”大于“生成新对象的开销”,从而使性能降低的情况。但是对于生成时开销可观的对象,池化技术就是提高性能的有效策略了。
三、使用介绍
3.1、 相关概念:
- 链接池/对象池(ObjectPool):用于存放链接对象的一个池子(集合)。
通常用数组或者List对象.durid用两个数组分别保存活跃的链接和空闲链接.
在commons-pool2中:
-
- 用双端阻塞队列LinkedBlockingDeque保存空闲链接。
- 用ConcurrentHashMap保存所有的链接.
- 对象工厂(PooledObjectFactory):连接池工厂,用于产生一个新的对象。
- 链接对象/池中对象(PooledObject):链接池里面存放的对象。
3.2、相关API
3.2.1 GenericObjectPool
GenericObjectPool继承BaseGenericObjectPool实现ObjectPool,通常用此实现来作为默认的连接池对象。
ObjectPool连接池的最上层接口,定义了一个连接池必须具备的方法,比如借用一个连接对象T borrowObject(),归还一个使用后的连接对象void returnObject(T obj)。ObjectPool的源码:
package org.apache.commons.pool2; public interface ObjectPool<T> { //从pool中借出pooledObject T borrowObject() throws Exception, NoSuchElementException, IllegalStateException; //将pooledObject还给pool void returnObject(T obj) throws Exception; //使池中使对象无效,根据约定,obj必须使用在实现或子接口中定义的borrow对象或相关方法获得。当已被借来的对象(由于异常或其他问题)被确定为无效时,应该使用此方法。 void invalidateObject(T obj) throws Exception; //使用工厂或其他依赖于实现的机制创建对象,使其钝化,然后将其放入空闲对象池中。addObject对于用空闲对象“预加载”池非常有用。(可选操作)。 void addObject() throws Exception, IllegalStateException, UnsupportedOperationException; //返回此池中当前空闲的实例数。这可以看作是不创建任何新实例就可以借用的对象数量的近似值。如果此信息不可用,则返回一个负值。 int getNumIdle(); //返回当前从这个池中借用的实例数。如果此信息不可用,则返回一个负值。 int getNumActive(); //清除池中闲置的任何对象,释放任何相关资源(可选操作)。清除的空闲对象必须是PooledObjectFactory.destroyObject(PooledObject)。 void clear() throws Exception, UnsupportedOperationException; //关闭此池,并释放与之关联的任何资源。 //在池上调用此方法后调用addObject或borrow对象将导致它们抛出IllegalStateException。 //如果不是所有的资源都可以被释放,那么实现应该无声地失败。 void close(); }
BaseGenericObjectPool一个抽象类,主要实现了两个功能:
- 注册JMX
- 管理一个连接池驱逐线程,此线程调用GenericObjectPool的evict()驱逐超过最大生命周期的连接对象。evict()是是个抽象方法,由GenericObjectPool子类实现。
3.2.2 PooledObjectFactory
PooledObjectFactory用于生成连接对象的工厂接口。
package org.apache.commons.pool2; public interface PooledObjectFactory<T> { //1、创建一个可以由池提供服务的实例,并将其封装到池管理的PooledObject中。 PooledObject<T> makeObject() throws Exception; //销毁池不再需要的实例。 //对于这个方法的实现来说,重要的是要意识到obj处于什么状态并不能保证,并且实现应该准备好处理意外错误。 //此外,实现必须考虑到,丢失给垃圾收集器的实例可能永远不会被销毁。 void destroyObject(PooledObject<T> p) throws Exception; //确保池可以安全地返回实例。 boolean validateObject(PooledObject<T> p); //重新初始化要由池返回的实例。 void activateObject(PooledObject<T> p) throws Exception; //取消要返回到空闲对象池的实例的初始化。 void passivateObject(PooledObject<T> p) throws Exception; }
该接口包含以下功能:
- 产生一个连接对象:PooledObject<T> makeObject() throws Exception;
在连接池初始化时初始化最小连接数
驱逐线程驱逐完过期连接后池中连接数<最小连接数,需重新生成连接,使连接数达到池中最小连接数
获取新的连接时,池中连接对象均被占用,但当前连接数<总连接数时
一般当遇到以上3中情况时需要调用该方法产生一个新的连接
2.销毁一个连接对象:void destroyObject(PooledObject<T> p) throws Exception;
调用该方法销毁一个连接对象。对于实现这个方法来说非常重要的是要考虑到处理异常情况,另外实现必须考虑一个实例如果与垃圾回收器失去联系那么永远不会被销毁。
3、校验方法boolean validateObject(PooledObject<T> p);
此方法主要用于校验一个连接是否可用,比如在borrow一个连接时或者return一个连接时,调用该方法检测连接是否可用。需要注意的是校验方法只会作用于激活的对象实例上。通常的做法是在连接对象空闲的时候进行校验,而不是在使用的时候进行校验,因为这样会影响性能。
4、重新激活一个对象void activateObject(PooledObject<T> p) throws Exception;
激活一个对象,在向对象池归还被钝化过的对象时调用该方法。
5、钝化一个对象void passivateObject(PooledObject<T> p) throws Exception;
钝化一个对象。在向对象池归还一个对象是会调用这个方法。
当一个对象从借用到归还需经过如下流程:
3.2.3 DefaultPooledObject
DefaultPooledObject 默认的对象包装器用于跟踪其他信息,例如状态。实现了PooledObject。
3.2.4 GenericKeyedObjectPool
类似于GenericObjectPool,每个key对应一个subPool,,每个key对应一个GenericObjectPool。它用于区别不同类型的对象。实际上是通过ConcurrentHashMap来保存每个key的PooledObject。
3.2.5 BaseObjectPoolConfig
一个参数配置抽象类,用于自定义对象池参数。
boolean lifo
对象池存储空闲对象是使用的LinkedBlockingDeque,它本质上是一个支持FIFO和FILO的双向的队列,common-pool2中的LinkedBlockingDeque不是Java原生的队列,而有common-pool2重新写的一个双向队列。如果为true,表示使用FIFO获取对象。
boolean fairness
common-pool2实现的LinkedBlockingDeque双向阻塞队列使用的是Lock锁。这个参数就是表示在实例化一个LinkedBlockingDeque时,是否使用lock的公平锁。默认值是false。
long maxWaitMillis
当没有空闲连接时,获取一个对象的最大等待时间。如果这个值小于0,则永不超时,一直等待,直到有空闲对象到来。如果大于0,则等待maxWaitMillis长时间,如果没有空闲对象,将抛出NoSuchElementException异常。默认值是-1;可以根据需要自己调整,单位是毫秒。
long minEvictableIdleTimeMillis
对象最小的空闲时间。如果为小于等于0,最Long的最大值,如果大于0,当空闲的时间大于这个值时,执行移除这个对象操作。默认值是1000L * 60L * 30L;即30分钟。可以避免(连接)泄漏。
long evictorShutdownTimeoutMillis
shutdown驱逐线程的超时时间。当创建驱逐线(evictor)程时,如发现已有一个evictor正在运行则会停止该evictor,evictorShutdownTimeoutMillis表示当前线程需等待多长时间让ScheduledThreadPoolExecutor(evictor继承自TimerTask,由ScheduledThreadPoolExecutor进行调度)停止该evictor线程。
long softMinEvictableIdleTimeMillis
对象最小的空间时间,如果小于等于0,取Long的最大值,如果大于0,当对象的空闲时间超过这个值,并且当前空闲对象的数量大于最小空闲数量(minIdle)时,执行移除操作。这个和上面的minEvictableIdleTimeMillis的区别是,它会保留最小的空闲对象数量。而上面的不会,是强制性移除的。默认值是-1;
int numTestsPerEvictionRun
检测空闲对象线程每次检测的空闲对象的数量。默认值是3;如果这个值小于0,则每次检测的空闲对象数量等于当前空闲对象数量除以这个值的绝对值,并对结果向上取整。
boolean testOnCreate
在创建对象时检测对象是否有效,true是,默认值是false。做了这个配置会降低性能。
boolean testOnBorrow
在从对象池获取对象时是否检测对象有效,true是;默认值是false。做了这个配置会降低性能。
boolean testOnReturn
在向对象池中归还对象时是否检测对象有效,true是,默认值是false。做了这个配置会降低性能。
boolean testWhileIdle
在检测空闲对象线程检测到对象不需要移除时,是否检测对象的有效性。true是,默认值是false。建议配置为true,不影响性能,并且保证安全性。
long timeBetweenEvictionRunsMillis
空闲对象检测线程的执行周期,即多长时候执行一次空闲对象检测。单位是毫秒数。如果小于等于0,则不执行检测线程。默认值是-1;
boolean blockWhenExhausted
当对象池没有空闲对象时,新的获取对象的请求是否阻塞。true阻塞。默认值是true;
boolean jmxEnabled
是否注册JMX
String jmxNamePrefix
JMX前缀
String jmxNameBase
使用base + jmxNamePrefix + i来生成ObjectName
int maxTotal
对象池中管理的最多对象个数。默认值是8。
int maxIdle
对象池中最大的空闲对象个数。默认值是8。
int minIdle
对象池中最小的空闲对象个数。默认值是0。
四、 实现自己的连接池
如上,要实现一个连接池首先需要3个基本的类,PooledObject池中对象,PooledObjectFactory对象工厂,ObjectPool对象池。由于ObjectPool缓存的是一个对象的包装类型即PooledObject,所以在PooledObjectFactory获得对象的时候需将实际对象进行包装。
3.1 创建连接对象
这里定义一个Connection对象,通过传入一个int值作为内部ID
public class Connection { private int id; public Connection(int id) { this.id = id; } @Override public String toString() { return "Connection{" + "id=" + id + '}'; } }
3.2 实现PooledObjectFactory
这里选择继承默认的BasePooledObjectFactory,此抽象类独立了2个抽象方法,其余方法全部为空实现,根据自己的需求进行实现。
public class ConnectionFactory extends BasePooledObjectFactory<Connection> { private AtomicInteger idCount = new AtomicInteger(1); @Override public Connection create() throws Exception { return new Connection(idCount.getAndAdd(1)); } @Override public PooledObject<Connection> wrap(Connection conn) { //包装实际对象 return new DefaultPooledObject<>(conn); } }
3.3 实现ObjectPool
同样选择继承抽象实现GenericObjectPool,该抽象类实现了连接池的大多数功能,基本能够满足多数需求,如需定制开发自行实现ObjectPool
public class ConnectionPool extends GenericObjectPool<Connection> { public ConnectionPool(PooledObjectFactory<Connection> factory) { super(factory); } public ConnectionPool(PooledObjectFactory<Connection> factory, GenericObjectPoolConfig config) { super(factory, config); } public ConnectionPool(PooledObjectFactory<Connection> factory, GenericObjectPoolConfig config, AbandonedConfig abandonedConfig) { super(factory, config, abandonedConfig); } }
3.4测试
public class Main { public static void main(String[] args) throws Exception { ConnectionFactory orderFactory = new ConnectionFactory(); GenericObjectPoolConfig config = new GenericObjectPoolConfig(); config.setMaxTotal(5); //设置获取连接超时时间 config.setMaxWaitMillis(1000); ConnectionPool connectionPool = new ConnectionPool(orderFactory, config); for (int i = 0; i < 7; i++) { Connection o = connectionPool.borrowObject(); System.out.println("brrow a connection: " + o +" active connection:"+connectionPool.getNumActive()); //connectionPool.returnObject(o); } } }
在上面的代码中我们将连接池的最大值设置5,循环创建7个对象,获取对象的超时时间设置为1000ms,注意此处先注释掉returnObject(o)方法。运行代码将会得到如下信息:
brrow a connection: Connection{id=1} active connection:1
brrow a connection: Connection{id=2} active connection:2
brrow a connection: Connection{id=3} active connection:3
brrow a connection: Connection{id=4} active connection:4
brrow a connection: Connection{id=5} active connection:5
Exception in thread "main" java.util.NoSuchElementException: Timeout waiting for idle object
at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:449)
at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:363)
at com.pool.Main.main(Main.java:21)
Process finished with exit code 1
可以看到当获取第6个连接时抛出了异常,提示获得连接超时。当active connection达到MaxTotal时此时连接数达到上限,不能再继续创建新的连接,需要等待空闲连接(释放连接),但是因为我们并没有释放掉借用的连接,造成了连接泄漏,并设置了获取连接的超时时间,所以当达到超时时候后就会抛出等待超时异常。
我们修改上面的代码,将注释的代码打开,继续运行:
brrow a connection: Connection{id=1} active connection:1
brrow a connection: Connection{id=1} active connection:1
brrow a connection: Connection{id=1} active connection:1
brrow a connection: Connection{id=1} active connection:1
brrow a connection: Connection{id=1} active connection:1
brrow a connection: Connection{id=1} active connection:1
brrow a connection: Connection{id=1} active connection:1
Process finished with exit code 0
可以发现active connection始终=1,因为我们借用连接后及时的归还(释放)了连接,避免了连接泄漏。
以上就是使用common-pool2的最简单实现,在后面会对common-pool2的源码进行分析解读,了解基本的连接池实现原理及druid连接池和common-pool2的一些设计上对比。
————————————————
版权声明:本文为CSDN博主「蓝墨49」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq447995687/article/details/80233621