commons-pool2对象池实战

1.why

我们在服务器开发的过程中,往往会有一些对象,它的创建和初始化需要的时间比较长,比如数据库连接,网络IO,大数据对象等。在大量使用这些对象时,如果不采用一些技术优化,就会造成一些不可忽略的性能影响。一种办法就是使用对象池,每次创建的对象并不实际销毁,而是缓存在对象池中,下次使用的时候,不用再重新创建,直接从对象池的缓存中取即可。为了避免重新造轮子,我们可以使用优秀的开源对象池化组件apache-commons-pool2,它对对象池化操作进行了很好的封装,我们只需要根据自己的业务需求重写或实现部分接口即可,使用它可以快速的创建一个方便,简单,强大对象连接池管理类。

2.what

  • GenericObjectPool类
    这个是对象池实现的核心类,它实现了对对象池的管理,是一个基本的对象池实现

基本方法:

// 对象使用完之后,归还到对象池
@Override
public void returnObject(final T obj) {
    .....
}
// 从对象池中获取一个对象
@Override
public T borrowObject() throws Exception {
    return borrowObject(getMaxWaitMillis());
}

其它还有一些方法,比如关闭对象池,销毁对象池,获取对象池中空闲的对象个数等,可以自行查看API

  • PooledObjectFactory接口
    这个接口是我们要实现的,它对要实现对象池化的对象做了一些管理。这个工厂接口就是为了让我们根据自己的业务创建和管理要对象池化的对象。当然也可以继承默认的抽象类:BasePooledObjectFactory ,下面会提到。
PooledObject<T> makeObject() throws Exception;

这个方法是用来创建一个对象,当在GenericObjectPool类中调用borrowObject方法时,如果当前对象池中没有空闲的对象,GenericObjectPool会调用这个方法,创建一个对象,并把这个对象封装到PooledObject类中,并交给对象池管理。

void destroyObject(PooledObject<T> p) throws Exception;

销毁对象,当对象池检测到某个对象的空闲时间(idle)超时,或使用完对象归还到对象池之前被检测到对象已经无效时,就会调用这个方法销毁对象。对象的销毁一般和业务相关,但必须明确的是,当调用这个方法之后,对象的生命周期必须结果。如果是对象是线程,线程必须已结束,如果是socket,socket必须已close,如果是文件操作,文件数据必须已flush,且文件正常关闭。

boolean validateObject(PooledObject<T> p);

检测一个对象是否有效。在对象池中的对象必须是有效的,这个有效的概念是,从对象池中拿出的对象是可用的。比如,如果是socket,那么必须保证socket是连接可用的。在从对象池获取对象或归还对象到对象池时,会调用这个方法,判断对象是否有效,如果无效就会销毁。

void activateObject(PooledObject<T> p) throws Exception;

激活一个对象或者说启动对象的某些操作。比如,如果对象是socket,如果socket没有连接,或意外断开了,可以在这里启动socket的连接。它会在检测空闲对象的时候,如果设置了测试空闲对象是否可以用,就会调用这个方法,在borrowObject的时候也会调用。另外,如果对象是一个包含参数的对象,可以在这里进行初始化。让使用者感觉这是一个新创建的对象一样。

void passivateObject(PooledObject<T> p) throws Exception;

钝化一个对象。在向对象池归还一个对象是会调用这个方法。这里可以对对象做一些清理操作。比如清理掉过期的数据,下次获得对象时,不受旧数据的影响。

一般来说activateObject和passivateObject是成对出现的。前者是在对象从对象池取出时做一些操作,后者是在对象归还到对象池做一些操作,可以根据自己的业务需要进行取舍。

  • BasePooledObjectFactory类
    这个抽象类是PooledObjectFactory 接口的空现,并且透出了两个抽象方法必须实现。
public abstract T create() throws Exception;

创建一个对象实例,可以被wrap成一个PooledObject,这个方法必须支持并发和多线程。

public abstract PooledObject<T> wrap(T obj);

把一个对象包装为一个PooledObject,此方法只在调用borrowObject方法的时候,且返回一个全新对象的时候执行,此方法处理create()方法的返回值。常见的处理方式是new DefaultPooledObject<>(obj)。可以在包装前进行其他逻辑的处理

  • GenericKeyedObjectPool带Key的对象池
    这种对象池和前面的GenericObjectPool对象池操作是一样的,不同的是对应的每个方法带一个key参数。你可以把这个GenericKeyedObjectPool的对象池看作是一个map的GenericObjectPool,每个key对应一个GenericObjectPool。它用于区别不同类型的对象。比如数据库连接,有可能会连接到不同地址的数据库上面。就可以用这个区分。
  • GenericObjectPoolConfig参数配置类
    这个类允许使用者对对象池的一些参数进行调整,根据需要定制对象池。下面说逐一说一下每个参数的含义。

lifo:对象池存储空闲对象是使用的LinkedBlockingDeque,它本质上是一个支持FIFO和FILO的双向的队列,common-pool2中的LinkedBlockingDeque不是Java原生的队列,而有common-pool2重新写的一个双向队列。如果为true,表示使用FIFO获取对象。默认值是true.建议使用默认值。

fairness:common-pool2实现的LinkedBlockingDeque双向阻塞队列使用的是Lock锁。这个参数就是表示在实例化一个LinkedBlockingDeque时,是否使用lock的公平锁。默认值是false,建议使用默认值。

maxWaitMillis:当没有空闲连接时,获取一个对象的最大等待时间。如果这个值小于0,则永不超时,一直等待,直到有空闲对象到来。如果大于0,则等待maxWaitMillis长时间,如果没有空闲对象,将抛出NoSuchElementException异常。默认值是-1;可以根据需要自己调整,单位是毫秒。

minEvictableIdleTimeMillis:对象最小的空闲时间。如果为小于等于0,最Long的最大值,如果大于0,当空闲的时间大于这个值时,执行移除这个对象操作。默认值是1000L * 60L * 30L;即30分钟。这个参数是强制性的,只要空闲时间超过这个值,就会移除。

softMinEvictableIdleTimeMillis:对象最小的空间时间,如果小于等于0,取Long的最大值,如果大于0,当对象的空闲时间超过这个值,并且当前空闲对象的数量大于最小空闲数量(minIdle)时,执行移除操作。这个和上面的minEvictableIdleTimeMillis的区别是,它会保留最小的空闲对象数量。而上面的不会,是强制性移除的。默认值是-1;

numTestsPerEvictionRun:检测空闲对象线程每次检测的空闲对象的数量。默认值是3;如果这个值小于0,则每次检测的空闲对象数量等于当前空闲对象数量除以这个值的绝对值,并对结果向上取整。

testOnCreate:在创建对象时检测对象是否有效,true是,默认值是false。

testOnBorrow:在从对象池获取对象时是否检测对象有效,true是;默认值是false。

testOnReturn:在向对象池中归还对象时是否检测对象有效,true是,默认值是false。

testWhileIdle:在检测空闲对象线程检测到对象不需要移除时,是否检测对象的有效性。true是,默认值是false。

timeBetweenEvictionRunsMillis:空闲对象检测线程的执行周期,即多长时候执行一次空闲对象检测。单位是毫秒数。如果小于等于0,则不执行检测线程。默认值是-1;

blockWhenExhausted:当对象池没有空闲对象时,新的获取对象的请求是否阻塞。true阻塞。默认值是true;

maxTotal:对象池中管理的最多对象个数。默认值是8。

maxIdle:对象池中最大的空闲对象个数。默认值是8。

minIdle:对象池中最小的空闲对象个数。默认值是0。

以上就是common-pool2对象池的配置参数,使用的时候可以根据自己的需要进行调整。

3.how

3.0 导入依赖包

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.5.0</version>
</dependency>

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的一些设计上对比。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值