使用Apache Commons Pool2创建Java对象池

前言

在Java应用程序中,频繁地创建和销毁对象会消耗大量的内存和CPU资源,影响应用程序的性能和可伸缩性。为了解决这个问题,我们可以使用对象池技术,将对象存储在池中,在需要的时候从池中获取,使用完毕后将对象归还到池中。Apache Commons Pool2是一个流行的开源对象池实现,提供了丰富的功能和配置选项,可以满足不同应用程序的需求。

Commons Pool2

Commons Pool2是一个流行的开源对象池实现,提供了丰富的接口和配置选项,使得实现对象池变得非常容易,并且可以根据具体的业务需求进行灵活的配置。Commons Pool2还支持多线程共享对象池中的对象,避免线程之间的竞争,从而提高应用程序的并发性能。

实现一个简单的对象池

下面让我们通过理解和使用Commons Pool2的BasePooledObjectFactory、GenericObjectPool和GenericObjectPoolConfig三个核心接口和类来实现一个对象池。

引入依赖

<dependency>
    <groupId>org.apache.commonsgroupId>
    <artifactId>commons-pool2artifactId>
    <version>2.11.0version>
dependency>

BasePooledObjectFactory

首先,让我们看一下BasePooledObjectFactory类。这个类是一个抽象类,可以用于创建和销毁池中的对象。要使用这个类,我们需要继承它并重写以下方法:
create():用于创建对象,返回一个新的对象。
wrap(T obj):用于包装对象,返回一个PooledObject对象,其中包含了对象本身以及对象状态信息。
validateObject(PooledObject p):用于验证对象是否可用,返回一个布尔值,表示对象是否可用。
destroyObject(PooledObject p):用于销毁对象。

让我们先定义一个对象类,可以在里面编写一些对象的创建、销毁和是否可用等方法,用于示例展示或者测试用途的。

public class MyObject {
    private String name;

    public MyObject(String name) {
        this.name = name;
    }

    public void create() {
        System.out.println("ThreadName:"+ Thread.currentThread().getName() + " 对象:" + name + "正在被创建。。。。。。");

    }

    public void destroy() {
        System.out.println("ThreadName:"+ Thread.currentThread().getName() + " 对象:" + name + "正在被销毁。。。。。。");

    }

    public boolean isValid() {
        System.out.println("ThreadName:"+ Thread.currentThread().getName() + " 对象" + name + "正在检验是否可用。。。。。。");

        return true;
    }

}
public class MyObjectFactory extends BasePooledObjectFactory<MyObject> {
    @Override
    public MyObject create() throws Exception {
        // 创建一个新的MyObject对象
        MyObject myObject = new MyObject(UUID.randomUUID().toString());
        myObject.create();
        return myObject;
    }

    @Override
    public PooledObject<MyObject> wrap(MyObject myObject) {
        // 将MyObject对象封装到一个PooledObject对象中并返回
        return new DefaultPooledObject<>(myObject);
    }

    @Override
    public void destroyObject(PooledObject<MyObject> pooledObject) throws Exception {
        // 销毁对象
        MyObject myObject = pooledObject.getObject();
        myObject.destroy();
    }

    @Override
    public boolean validateObject(PooledObject<MyObject> pooledObject) {
        // 验证对象是否可用
        MyObject myObject = pooledObject.getObject();
        return myObject.isValid();
    }
}

GenericObjectPoolConfig

接下来,让我们看一下GenericObjectPoolConfig类。这个类是一个配置类,用于配置对象池的属性,例如池大小、最大等待时间、是否允许对象为null等。使用这个类,我们可以根据应用程序的需求来自定义对象池的行为和配置。

下面列出一些配置的默认值和推荐值以及说明。

GenericObjectPoolConfig<MyObject> config = new GenericObjectPoolConfig<>();
config.setMaxTotal(8);          // 对象池中最大对象数
config.setMaxIdle(4);           // 对象池中最大空闲对象数
config.setMinIdle(2);           // 对象池中最小空闲对象数
config.setBlockWhenExhausted(true);  // 当对象池耗尽时,是否等待获取对象
config.setMaxWaitMillis(-1L);   // 对象池没有对象可用时,最大等待时间(单位:毫秒),-1表示无限等待
config.setTestOnCreate(false);  // 创建对象时是否进行对象有效性检查
config.setTestOnBorrow(false);  // 借出对象时是否进行对象有效性检查
config.setTestOnReturn(false);  // 归还对象时是否进行对象有效性检查
config.setTestWhileIdle(false); // 空闲时是否进行对象有效性检查
config.setMinEvictableIdleTimeMillis(1800000L);  // 对象池中对象的最小空闲时间(单位:毫秒)
config.setTimeBetweenEvictionRunsMillis(-1L);    // 后台对象回收器运行的时间间隔(单位:毫秒),-1表示不运行
config.setNumTestsPerEvictionRun(3);  // 后台对象回收器运行时检查对象的个数
config.setSoftMinEvictableIdleTimeMillis(-1L);  // 对象池中对象的最小空闲时间,当空闲对象的数目大于最小空闲数(minIdle)且空闲时间超过此值时,对象将被移除
config.setLifo(true);   // 是否使用后进先出原则借出对象
config.setFairness(false);  // 是否使用公平锁
config.setJmxEnabled(true); // 是否开启 JMX 监控

GenericObjectPool

最后,让我们看一下GenericObjectPool类。这个类是对象池的主要实现,可以用于从池中获取对象、将对象返回到池中以及获取当前池中的对象数量等。在创建对象池时,我们需要提供一个对象工厂类和一个对象池配置类。

使用GenericObjectPool类,我们可以使用以下方法:
borrowObject():用于从对象池中获取对象。如果对象池中没有可用对象,则此方法将阻塞,直到有可用对象为止。
returnObject(T obj):用于将对象返回到对象池中。
getNumIdle():用于获取当前空闲对象的数量。
getNumActive():用于获取当前正在使用的对象的数量。
close():用于关闭对象池。

我这里使用枚举来实现对象池的创建,因为枚举类型是一个线程安全的单例模式,可以避免多线程并发访问时可能出现的竞态条件和同步问题。确保在整个应用程序中只有一个对象池实例存在,可以有效地避免多个对象池实例之间可能出现的冲突和资源浪费问题。

public enum MyObjectPool {
    /**
     * 线程安全的单例
     */
    INSTANCE;

    private GenericObjectPool<MyObject> objectPool;

    MyObjectPool() {
        // 创建对象池配置
        GenericObjectPoolConfig<MyObject> poolConfig = new GenericObjectPoolConfig<>();
        // 对象池中最大对象数
        poolConfig.setMaxTotal(8);
        // 对象池中最小空闲对象数
        poolConfig.setMinIdle(2);
        // 对象池中最大空闲对象数
        poolConfig.setMaxIdle(4);
        // 当对象池耗尽时,是否等待获取对象
        poolConfig.setBlockWhenExhausted(true);
        // 创建对象时是否进行对象有效性检查
        poolConfig.setTestOnCreate(true);
        // 借出对象时是否进行对象有效性检查
        poolConfig.setTestOnBorrow(true);
        // 归还对象时是否进行对象有效性检查
        poolConfig.setTestOnReturn(true);
        // 空闲时是否进行对象有效性检查
        poolConfig.setTestWhileIdle(true);


        // 创建对象工厂
        MyObjectFactory objectFactory = new MyObjectFactory();

        // 创建对象池
        objectPool = new GenericObjectPool<>(objectFactory, poolConfig);
    }

    public MyObject borrowObject() throws Exception {
        // 从对象池中借出一个对象
        return objectPool.borrowObject();
    }

    public void returnObject(MyObject myObject) {
        // 将对象归还给对象池
        objectPool.returnObject(myObject);
    }

    public int getNumActive() throws Exception {
        // 从对象池中借出一个对象
        return objectPool.getNumActive();
    }

    public int getNumIdle() throws Exception {
        // 从对象池中借出一个对象
        return objectPool.getNumIdle();
    }
}

测试

下面我们编写一个简单的测试方法,模拟单线程和多线程两种情况下,对象池管理对象的借出和归还的情况

public class PoolTest {
    public static void main(String[] args) throws Exception {
//        singleTest();
        threadTest();
    }

    public static void singleTest() throws Exception{
        MyObjectPool myObjectPool = MyObjectPool.INSTANCE;


        numActiveAndNumIdle(myObjectPool);
        Thread.sleep(1000);

        MyObject obj = myObjectPool.borrowObject();
        System.out.println("ThreadName:"+ Thread.currentThread().getName() + " borrowed: " + JSONObject.toJSONString(obj));
        Thread.sleep(1000);

        numActiveAndNumIdle(myObjectPool);
        Thread.sleep(1000);

        myObjectPool.returnObject(obj);
        System.out.println("ThreadName:"+ Thread.currentThread().getName() + " returned: " + JSONObject.toJSONString(obj));
        Thread.sleep(1000);

        numActiveAndNumIdle(myObjectPool);

    }

    private static void numActiveAndNumIdle(MyObjectPool myObjectPool) throws Exception{
        int numActive = myObjectPool.getNumActive();
        int numIdle = myObjectPool.getNumIdle();
        System.out.println("ThreadName:"+ Thread.currentThread().getName() + " numActive:" + numActive + " numIdle:" + numIdle);
    }

    public static void threadTest() throws Exception{
        ExecutorService executorService = Executors.newFixedThreadPool(9);

        for (int i = 0; i < 20; i++) {
            executorService.submit(() -> {
                try {
                    singleTest();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }

        executorService.shutdown();
        executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
    }


}

总结

相比于其他实现对象池的技术,使用 Commons Pool2 实现对象池的优点是:它提供了完整的对象池管理功能,包括对象的创建、初始化、借用、归还、清理和销毁等操作,并且支持多线程环境下的并发访问和线程安全。此外,Commons Pool2 还具有灵活的配置选项,可以根据具体场景对对象池的性能和资源消耗进行优化。

缺点是,使用 Commons Pool2 实现对象池需要引入额外的依赖,增加了项目的复杂性。此外,实现和配置对象池需要一定的技术能力,需要了解对象池的原理和相关的配置参数,否则可能会导致对象池的性能和稳定性问题。需要注意的问题包括对象池的配置参数,对象池的线程安全性,对象的有效性检查和对象的回收策略等。

你可能感兴趣的:(软件开发,java,笔记,java,设计模式,apache,Commons-Pool2,对象池)