利用commons-pool2自定义对象池
commons-pool2是Apache下一个开源的公共资源池。我们可以根据它来快速的建立一个自己的对象池。
1. 相关概念:
- 链接池/对象池(ObjectPool):用于存放链接对象的一个池子(集合),通常用数组或者List对象.durid用两个数组分别保存活跃的链接和空闲链接.commons-pool2用双端阻塞队列LinkedBlockingDeque保存空闲链接
用ConcurrentHashMap保存所有的链接. - 对象工厂(PooledObjectFactory):连接池工厂,用于产生一个新的链接对象.
- 链接对象/池中对象(PooledObject):链接池里面存放的对象.
2.相关API
2.1 GenericObjectPool
GenericObjectPool继承BaseGenericObjectPool实现ObjectPool,通常用此实现来作为默认的连接池对象。
BaseGenericObjectPool一个抽象类,主要实现了两个功能:
- 注册JMX
- 管理一个连接池驱逐线程,此线程调用GenericObjectPool的evict()驱逐超过最大生命周期的连接对象。
ObjectPool连接池的最上层接口,定义了一个连接池必须具备的方法,比如借用一个连接对象T borrowObject(),归还一个使用后的连接对象void returnObject(T obj)。
2.2 PooledObjectFactory
PooledObjectFactory用于生成连接对象的工厂接口。该接口包含以下功能:
产生一个连接对象:
PooledObject<T> makeObject() throws Exception;
在连接池初始化时初始化最小连接数
驱逐线程驱逐完过期连接后池中连接数<最小连接数,需重新生成连接,使连接数达到池中最小连接数
获取新的连接时,池中连接对象均被占用,但当前连接数<总连接数时
一般当遇到以上3中情况时需要调用该方法产生一个新的连接销毁一个连接对象:
void destroyObject(PooledObject<T> p) throws Exception;
调用该方法销毁一个连接对象。对于实现这个方法来说非常重要的是要考虑到处理异常情况,另外实现必须考虑一个实例如果与垃圾回收器失去联系那么永远不会被销毁。
校验方法
boolean validateObject(PooledObject<T> p);
此方法主要用于校验一个连接是否可用,比如在borrow一个连接时或者return一个连接时,调用该方法检测连接是否可用。需要注意的是校验方法只会作用于激活的对象实例上。通常的做法是在连接对象空闲的时候进行校验,而不是在使用的时候进行校验,因为这样会影响性能。
重新激活一个对象
void activateObject(PooledObject<T> p) throws Exception;
激活一个对象,在向对象池归还被钝化过的对象时调用该方法。
钝化一个对象
void passivateObject(PooledObject<T> p) throws Exception;
钝化一个对象。在向对象池归还一个对象是会调用这个方法。
当一个对象从借用到归还需经过如下流程:
2.3 DefaultPooledObject
DefaultPooledObject 默认的对象包装器用于跟踪其他信息,例如状态。实现了PooledObject。
2.4 GenericKeyedObjectPool
类似于GenericObjectPool,每个key对应一个subPool,,每个key对应一个GenericObjectPool。它用于区别不同类型的对象。实际上是通过ConcurrentHashMap来保存每个key的PooledObject。
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. 实现自己的连接池
如上,要实现一个连接池首先需要3个基本的类,PooledObjec池中对象,PooledObjectFactory对象工厂,ObjectPool对象池。由于ObjectPool缓存的是一个对象的包装类型即PooledObjec,所以在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的一些设计上对比。