在实际应用中,我们会接触诸如线程池,数据库连接池,各种池化技术实现的池,那是否可以实现一个通用的池,可以涵盖线程池,数据库连接池等等功能呢? 下面我们来尝试下,老规矩,我们先编写测试程序,通过测试程序看一下我们的需求是怎样的,然后该如何去实现这个通用的池.以数据库连接为例,
public class Main {
public static void main(String[] args){
Pool pool = new GenericPool(xxx,
xxx,
xxx);
Connection connection = pool.borrowObject();
//业务处理
//.......................
pool.returnObject(connection);
//关闭池
pool.shutdown();
}
}
从上我们可以总结出一个池它应该有的基本功能如下:
1,允许程序从池中"借"走一个元素
2,有借有还,程序用完后需要归还元素到池中
3,池关闭
从上可以看出,一个池的基本行为有借,还,关闭三种行为,so,我们首先定义一个接口,用于描述这种行为:
public interface Pool {
/**
* 获取对象
* @return
*/
public T borrowObject();
/**
* 归还对象
* @param t
*/
public void returnObject(T t);
/**
* 关闭池
*/
void shutdown();
}
为了使我们的池通用,采用了泛型来描述被池化的对象。接下来,我们得想下,该如何去实现这个接口,并且我们的实现能够尽可能的通用。
首先分析第一种行为 “”借”,当从池中取出元素时,我们需要校验下该元素是否可用,有可能在池里待待久了,某些属性或行为已经发生了变化,而元素却没有及时感知到
第二中行为,同上,在放入池子中之前,需要校验下该池化对象是否可用
第三种行为,池关闭依赖于池的具体实现,所以该方法需要放置在具体的实现类中
基于上面的分析,我们很容易发现其中存在共性,我们将这些共性抽取出来,提供一个基础的实现:
public abstract class AbstractPool implements Pool {
/**
* 从池中取出来的时候,需要校验当前的对象是否可用
* @return
*/
@Override
public T borrowObject() {
T t = borrowObjectFromPool();
if(isValid(t)){
return t;
}else{
handleInvalidObject(t);
}
return null;
}
/**
* 同样在归还的时候需要校验当前对象是否可用
* @param t
*/
@Override
public void returnObject(T t) {
if(isValid(t)){
returnObjectToPool(t);
}else{
handleInvalidObject(t);
}
}
protected abstract void returnObjectToPool(T t);
protected abstract void handleInvalidObject(T t);
protected abstract boolean isValid(T t);
protected abstract T borrowObjectFromPool();
}
再考虑下上面的几个abstract方法,其中借出和归还两个行为已经非常通用了,略过,主要思考下剩余的两个方法。判断一个对象是否有效的方法需要扩展下。比如如果这个池化的对象是数据库连接,那么判断是否有效时需要判断连接是否已关闭等,如果池化的对象是线程,那需要判断线程是否已中断。总之因为池化对象的不同,对应着不同的实现,handleInvalidObject方法也是同样的道理。所以我们新增一个接口,用于描述这两个行为
public interface Validator {
public boolean isValid(T t);
public void invalidate(T t);
}
这样,我们再构造对象池的时候,传入对应的Validator实现即可。回过头来,我们思考下shutdown方法,该方法也是依赖于具体池实现,那为什么不抽出一个接口来描述下这个行为呢?其实没有必要,因为在关闭池时,可以直接调用validator的invalidate方法清理池化的对象。接下来,就应该考虑如果去实现我们的池的功能了,这里我定义类GenericPool为我们的池,思考下我们这个池应该是怎样的。其实很简单,既然有了class,class中无外乎就包含成员变量和方法了,我们首先来看下它应该包含哪些成员变量:
1,池大小,能够容纳多少个对象
2,对象该放在何处呢,需要一个数据结构能够方便的进行操作
3,根据上面的描述,我们需要一个Validator来处理valid/invalid操作
4,也是最重要的一点,对象该如何产生呢,这个池需要知道对象是怎样产生的
至于这个类应该有哪些方法就不必再说了,上文中的AbstractPool已经能很清楚的说明了。
下面我们挨个解决上面的问题:
1,定义变量size,用于表示池的大小
2,定义变量pooledObjects,其类型为LinkedBlockingQueue,用于存放被池化的对象
3,定义变量validator,类型为Validator,需要有具体的实现
4,考虑到不同对象,产生的方式可能不用,这里我们将这个行为抽出来,定义一个接口
public interface ObjectFactory {
public T makeObject();
}
到此,我们就知道了GenericPool类大致应该是这样的:
public class GenericPool extends AbstractPool {
private int size;//池大小
private BlockingQueue pooledObjects;//池化的对象存放在哪里
private Validator validator;
private ObjectFactory objectFactory;
public GenericPool(int size, Validator validator, ObjectFactory objectFactory ){
this.size = size;
this.pooledObjects = new LinkedBlockingQueue();
this.validator = validator;
this.objectFactory = objectFactory;
init();
}
}
构造函数中init方法用于初始化池,向池中放入size个对象
private void init() {
for(int i=0;i
接下来,就是实现剩下的几个方法了:
@Override
public void shutdown() {
for(T t : pooledObjects){
validator.invalidate(t);
}
pooledObjects.clear();
pooledObjects = null;
validator = null;
objectFactory = null;
}
@Override
protected void returnObjectToPool(T t) {
if(isValid(t)){
pooledObjects.add(t);
}else{
handleInvalidObject(t);
}
}
@Override
protected void handleInvalidObject(T t) {
validator.invalidate(t);
}
@Override
protected boolean isValid(T t) {
return validator.isValid(t);
}
@Override
protected T borrowObjectFromPool() {
T t = null;
try {
t = pooledObjects.take();
} catch (InterruptedException e) {
throw new RuntimeException("interrupt happens during borrow object");
}
return t;
}
到此,主要的功能我们都实现了,还缺少的是Validator和ObjectFactory的具体实现了,这里我们首先以数据库连接为例,给个例子:
public class JdbcConnectionFactory implements ObjectFactory {
static {
try {
//加载驱动类
Class.forName("org.postgresql.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
private String url;
private String userName;
private String password;
public JdbcConnectionFactory(String url,String name,String pwd){
this.url = url;
this.userName = name;
this.password = pwd;
}
@Override
public Connection makeObject() {
try {
return DriverManager.getConnection(url,userName,password);
} catch (SQLException e) {
throw new RuntimeException("make object failed");
}
}
}
public class JdbcConnectionValidator implements Validator {
@Override
public boolean isValid(Connection connection) {
try {
return null!=connection && !connection.isClosed();
} catch (SQLException e) {
e.printStackTrace();
}
return false;
}
@Override
public void invalidate(Connection connection) {
if(null != connection){
try{
connection.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
}
接下来,更改下我们的测试类Main,我们看下测试的效果
public static void main(String[] args){
Pool pool = new GenericPool(3,
new JdbcConnectionValidator(),
new JdbcConnectionFactory("jdbc:postgresql://localhost:5432/postgres","postgres","postgres"));
//业务处理
for(int i=0;i<10;i++){
Connection connection = pool.borrowObject();
System.out.println("第"+i+"次获取到的连接:"+connection.toString());
pool.returnObject(connection);
}
//.......................
//关闭池
pool.shutdown();
我们在池放三个元素,然后模拟10次获取池化的对象,结果如下:
第0次获取到的连接:org.postgresql.jdbc4.Jdbc4Connection@7fca02
第1次获取到的连接:org.postgresql.jdbc4.Jdbc4Connection@1d8b871
第2次获取到的连接:org.postgresql.jdbc4.Jdbc4Connection@1026d9f
第3次获取到的连接:org.postgresql.jdbc4.Jdbc4Connection@7fca02
第4次获取到的连接:org.postgresql.jdbc4.Jdbc4Connection@1d8b871
第5次获取到的连接:org.postgresql.jdbc4.Jdbc4Connection@1026d9f
第6次获取到的连接:org.postgresql.jdbc4.Jdbc4Connection@7fca02
第7次获取到的连接:org.postgresql.jdbc4.Jdbc4Connection@1d8b871
第8次获取到的连接:org.postgresql.jdbc4.Jdbc4Connection@1026d9f
第9次获取到的连接:org.postgresql.jdbc4.Jdbc4Connection@7fca02
上面关于数据库连接的池化例子应该适用于绝大多数场景,但有一个场景不适合,那就是线程,主要有以下几个方面的问题:
1,线程一旦启动开始运行后,不能让线程退出,否则没法池化
2,获取池化的对象后,要能够让其执行对应的任务
基于以上两点,我们需要对线程进行封装,我们首先自定义池化的线程类PoolThread, 结构如下:
public class PoolThread {
private LinkedBlockingQueue task = new LinkedBlockingQueue();
private boolean started = false;
private boolean stopped = false;
private Thread thread = new Thread("poolThread-"+this.toString()){
@Override
public void run() {
while(!stopped){
try {
Runnable runnable = task.take();
runnable.run();
} catch (Exception e) {
e.printStackTrace();
}
}
}
};
public void run(Runnable runnable){
task.add(runnable);
if(!started){
thread.start();
started = true;
}
}
public Thread getThread() {
return thread;
}
public void setStopped(boolean stopped) {
this.stopped = stopped;
}
}
一个PoolThread对应一个真正的Thread,然后维护了一个队列,让真正的线程不停的从队列中获取任务,这样我们的PoolThread就可以被真正池化了。接下来,我们看下对应的Validator和ObjectFactory实现:
public class PoolThreadFactory implements ObjectFactory {
@Override
public PoolThread makeObject() {
return new PoolThread();
}
}
public class PoolThreadValidator implements Validator {
@Override
public boolean isValid(PoolThread poolThread) {
return !poolThread.getThread().isInterrupted();
}
@Override
public void invalidate(PoolThread poolThread) {
poolThread.getThread().interrupt();//中断线程
poolThread.setStopped(true);
LinkedBlockingQueue runnables = poolThread.getTask();
runnables.clear();
runnables = null;
}
}
接下来,我们更改下测试类,看下效果:
Pool pool = new GenericPool(3,new PoolThreadValidator(),new PoolThreadFactory());
for(int i=0;i<10;i++){
PoolThread thread = pool.borrowObject();
thread.run(new Runnable() {
@Override
public void run() {
System.out.println("task run in the thread:"+Thread.currentThread());
}
});
try {
Thread.sleep(1000);//等一秒再归还
}catch (Exception e){
e.printStackTrace();
}
pool.returnObject(thread);
}
//关闭池
pool.shutdown();
}
同样在池中放了三个线程,模拟十次请求,执行结果如下:
task run in the thread:Thread[poolThread-com.java.pool.test.PoolThread@1d74bb1,5,main]
task run in the thread:Thread[poolThread-com.java.pool.test.PoolThread@1aa7a6a,5,main]
task run in the thread:Thread[poolThread-com.java.pool.test.PoolThread@14ae5cd,5,main]
task run in the thread:Thread[poolThread-com.java.pool.test.PoolThread@1d74bb1,5,main]
task run in the thread:Thread[poolThread-com.java.pool.test.PoolThread@1aa7a6a,5,main]
task run in the thread:Thread[poolThread-com.java.pool.test.PoolThread@14ae5cd,5,main]
task run in the thread:Thread[poolThread-com.java.pool.test.PoolThread@1d74bb1,5,main]
task run in the thread:Thread[poolThread-com.java.pool.test.PoolThread@1aa7a6a,5,main]
task run in the thread:Thread[poolThread-com.java.pool.test.PoolThread@14ae5cd,5,main]
task run in the thread:Thread[poolThread-com.java.pool.test.PoolThread@1d74bb1,5,main]
ok,至此,池的功能我们实现了,下面我们从各个角度来思考下我们上面所实现的池的正确性,健壮性
1,异常处理,假设池中的对象有较长的时间未使用,并且已经失效但对象还未感知到,这样在数次调用borrow后,会导致线程卡在这里,该如何解决?
出现这种情况,如果发现pooledObjects队列为空,此时应该再次调用init方法,初始化池中的对象.同时必要的监控手段也要有
2,并发问题,是否线程安全?
比如多个线程调用shutdown方法,会出现并发问题,所以这里需要改造下:
public void synchronized shutdown() {
if(!shutdownCancelled){
for(T t : pooledObjects){
validator.invalidate(t);
}
pooledObjects.clear();
pooledObjects = null;
validator = null;
objectFactory = null;
}
}
用一个变量标识池是否已关闭,然后方法被关键字synchronized修饰即可
3,还有个问题就是在归还对象的时候,如果对同一个池化的对象做了多次归还,那么池中会有多个相同的对象,如何解决?
归还时先判断下队列中是否已经包含改对象,同时方法上应该加上synchronized关键字
4,假如程序员粗心,万一忘记归还操作,那这个池岂不是没有起到作用?
这个问题从java层面看,一是依靠程序员细心点,以及请别人共同code review的方式,二是将其作为一个jar包提供给开发人员使用,暴露一个接口/方法给开发人员实现业务逻辑,其余的诸如池管理等逻辑均由jar包中的工具实现,同时邮件各个开发人员(周知大家,可以避免背锅),最好不要自己重新写个池来用。