Apache-Common-Pool2中对象池的使用方式

发布时间:2024年01月14日

最近在工作中,对几个产品的技术落地进行梳理。这个过程中发现一些朋友对如何使用Apache的对象池存在一些误解。所以在写作“业务抽象”专题的空闲时间里,本人觉得有必要做一个关于对象池的知识点和坑点讲解。Apache Common-Pool2 组件最重要的功能,就是向开发人员提供一个稳定的、高性能的对象池。我们在实际开发过程中最常用、最典型的对象池就是数据库连接池。对象池可以在数据库连接对象被调用前,预先生成和管理一批可以使用的对象,用空间换时间的方式提升应用程序对数据库的操作性能。

1、对象池的使用场景

1.1、因为创建/验证对象造成较明显时间/性能消耗的场景

我们在应用程序开发过程中,经常使用基于TCP协议的数据库连接进行数据库操作。建立数据库连接实际上是一个比较复杂的网络通讯过程,而我们通过创建好的连接进行数据库操作(无论是读操作还是写操作)都只是整个TCP通讯过程中比较简短的一个过程,示例过程如下:
在这里插入图片描述
【此图来源于网络】

如上图所示,在正式执行数据库操作时,作为开发人员来说实际上只关心黄色部分“sql执行”的实际操作过程,而不太会承认其余部分所消耗的时间和自己书写的代码性能有关。由此可知,如果我们在需要进行数据库操作时,才进行数据库连接的创建,然后再进行正式的数据库操作,除上图中黄色区域所代表的的实际操作外其余部分消耗的时间都是一种浪费。

为了改进这个过程,我们可以先创建一批可以正常使用的数据库连接对象放在一旁,等我们需要进行数据库操作时,再从之前已经创建好的数据库连接对象中取得一个进行使用。当我们完成了数据库操作后,如果确定使用的数据库连接仍然能正常工作,就没有必要销毁这个数据库连接,而可以重新放回到一旁待下一次需要时再重复使用。这样的做法是典型的用空间换时间的方式提升运行性能:预先生成的一批对象占用了应用系统的内存,但是节约了对象创建、验证、销毁所消耗的时间。

1.2、对象所代表的资源在系统中是某种紧俏的存在

在进行图像特效处理工作中,例如图像马赛克、图像去模糊、图像羽化渲染等这样的处理工作。我们会采用多张图像捕捉处理卡在硬件级别进行处理,例如一台主机的一个操作系统同时管理5张图像处理卡。每一张图像处理卡在高级编程语言中,都被映射成一个独立的对象,且这样的对象一旦进行初始化就会消耗大量的时间。类似这样的对象,我们就要使用对象池进行管理。

在这里插入图片描述
这种场景下,通常的例子还有数据库连接、资源桶对象、租户网关等。

1.3、对象可以被重复使用,且具有幂等性控制的场景

能够被对象池所管理的实际工作对象,自身一定是可以被重复使用的。这个前提读者应该是很好理解的,因为如果不能被重复使用,那么对象从对象池重复取出使用时一定会报错。好消息是,在我们实际工作过程中遇到的大部分场景,其主要对象都是可以重复使用的,例如:图形图像处理对象、线程对象、加密处理对象、物资仓库对象、货运车辆对象等等。

但并不是对象能够被重复使用,开发人员就可以毫无顾忌的将它放入对象池进行重复使用了。开发人员还需要注意保持这些被重复使用对象的幂等性,例如在重复使用socket套接字时,为了保证幂等性每次使用完成后都需要保证套接字的缓存区不存在还没有发送的或者接受的信息,并且保证被归还的socket套接字没有被关闭;在重复使用货运车辆对象时,必须保证被归还的货运车辆上没有剩余的货物,且货运车辆还有油量。

2、关于对象池的常见误解

  • 误解:对象池和线程池是一个概念

高级语言一般会给开发人员提供线程池这样的功能进行使用(例如Java、C#、C++),虽然表现形式有所区别,但原理上都是将有限的线程资源映射成一个对象,并放入一个池集中进行管理;还将需要执行的工作任务也映射成对象,再根据一定逻辑使用管理的线程进行工作任务运行。这样来看线程池和对象池是否都是一个概念呢?

线程池是一种特殊的对象池,是对操作系统的线程资源进行管理,并能够驱动CPU线程进行工作的一种对象池。

我们一般意义上使用的对象池,是将有限的对象放置于一个共享的池中,然后在多线程场景下供多个线程分享取用,如下图所示:
在这里插入图片描述

但线程池是将待运行的任务放置到一个池中,并对有限数量的线程状态进行控制,最后根据应用程序的运行需要控制这些任务在受控线程上的运行。其核心要点在于可以控制线程的运行和线程上具体执行的工作任务。如下图所示:
在这里插入图片描述

当然我们可以将线程池和普通的线程池进行集成工作——这也是在实际工作中我们经常采用的一种保持系统性能平衡的方式,如下图所示:
在这里插入图片描述

  • 误解:对象池中的对象永远不需要进行销毁

对象池的主要功能是为了重用对象提高调用性能。这些对象所代表的资源本身可能是远程的、异步的、不稳定的。也就是说这些在应用程序中被映射成对象的资源本身是容易失效的,如果资源本身已经失效,代表这个资源存在于应用系统中的对象也应该失效。所以,对象池中的对象需要经常进行有效性验证,一旦验证失败就需要进行销毁——毕竟保证系统的稳定性要比保证系统的性能重要很多。大多数对象池也提供了不同场景下的验证、销毁策略,在应用程序运行时进行工作——比如后文中将进行使用讲解的Apache-Common Pool2 线程池。

  • 误解:只要使用了对象池,应用程序性能就会得到提升

实际上使用了对象池并保证对象池中对象的稳定,只是提高应用程序性能的一个前提。只有对象池和配合其使用的线程池达到一个稳定的状态,才能保证应用程序性能的提升。注意:这里使用的是线程池,而不是线程。也就是说站在本文的视角来看,在应用程序中只要使用了对象池,就应该有和其配合使用的线程池

对于对象池能够提升性能的另一个误解是,对象池中的对象可以在功能调用时提高工作性能。这种理解和运用对象池的初衷也有冲突:对象池的设计目标是用空间换时间的方式提高性能。哪一部分时间呢?就是对象初始化、对象销毁这部分操作所耗费的时间。哪一部分空间呢?就是这些被预先创建好的,可以正常工作的对象,将会存放在应用程序的内存区域(可能是本地的也可能是远程的)。也就是说对象所代表的资源被调用时,该消耗多少时间还是会消耗多少时间。

3、对象池使用实例讲解

这里本文对Apache-Common-Pool2中提供的对象池进行使用讲解。

3.1、定义一个功能对象(必须)

由于Apche的对象池本身基于AQS进行实现,具体来说是基于Apache自身撰写的一个线程安全的LinkedBlockingDeque(org.apache.commons.pool2.impl.LinkedBlockingDeque)队列集合进行的功能实现。所以开发人员定义的将要在对象池中使用的对象,不必自己再处理线程安全性(除非开发人员自身制造了一个危险的运行环境),按照一般的对象进行定义即可:

// 这是一个将要被放入对象池中的真实对象
public class MyObject implements Closeable {
  // 最大操作次数
  private static final Integer MAX_OP = 10;
  // 对象在系统中唯一的一个id信息
  private String uuid;
  // 该池化对象被借用的次数
  // 当借用次数达到一定数量后,该对象必须被回收,一遍保证缓存区不会溢出
  private AtomicInteger count = new AtomicInteger(0);
  // 表示是否已经关闭
  private AtomicBoolean closed = new AtomicBoolean(false);
  
  public MyObject() {
    // 为这个对象生成一个唯一的id
    this.uuid = UUID.randomUUID().toString();
  }

  public String getUuid() {
    return uuid;
  }
  
  // 这是该对象正式完成工作的调用方法
  public void doSomething() {
    try {
      // 测试代码,使用System.out即可
      System.out.println(String.format("...... 模拟工作过程 ...... 对象:%s,进行了工作 ......" , this.uuid));
      // 停止随机的毫秒数,模拟业务处理过程
      Random random = new Random();
      synchronized (this) {
        this.wait(random.nextInt(100));
      }
    } catch(InterruptedException e) {
      e.printStackTrace(System.out);
    } finally {
      this.count.incrementAndGet();
    }
  }
  
  /**
   * 对象能够被多次操作10次,10次后这个对象就必须关闭</br>
   * 这主要是为了模拟一些带有本身带有缓存、操作实现的操作对象
   */
  public boolean validate() {
    return this.count.get() < MAX_OP && !this.closed.get();
  }

  // 获取这个对象当前的使用次数
  public Integer count() {
    return this.count.intValue();
  }
  
  public void close() throws IOException {
    // 表示该对象不再有效
    this.closed.set(true);
  }
}

3.2、定义这个对象的池化对象(非必须)

池化对象是对真实对象的包装,其目的在于对真实对象的状态、借出时间、创建时间等状态信息进行记录和监控。如果在进行对象池化动作时,需要加入一些特定操作,则开发人员可以继承DefaultPooledObject类(推荐)或者重新实现PooledObject接口(不推荐)。但整个过程并不是必须的,事实上池化对象的记录和监控工作最好不要进行干扰。

以下是一种池化对象的扩展方式(这里只是做一个扩展方式的讲解,后续的代码中不会使用这个自定义的MyPooledObject类):

// 可以自定义的池化对象
public class MyPooledObject extends DefaultPooledObject<MyObject> {

  public MyPooledObject(MyObject object) {
    super(object);
  }
}

3.3、定义池化对象工厂(必须)

池化对象工厂定义了这个对象在对象池中被要求进行创建、借出、回收、检查操作时,该对象该如何进行进行工作。

// 池化对象工厂
public class MyObjectFactory extends BasePooledObjectFactory<MyObject> {
  /**
   * 对象池要求生成对象时,该方法被触发
   */
  @Override
  public MyObject create() throws Exception {
    MyObject myObject = new MyObject();
    String uuid = myObject.getUuid();
    System.out.println(String.format("生成一个新的对象 : %s " , uuid));
    return myObject;
  }
  /**
   * 用于为真实对象生成池化对象
   */
  @Override
  public PooledObject<MyObject> wrap(MyObject obj) {
    DefaultPooledObject<MyObject> myObject = new DefaultPooledObject<MyObject>(obj);
    return myObject;
  }
  /**
   * 当对象池在创建新的池化对象时、借出池化对象前、归还池化对象前,需要对被借出的池化对象进行验证,验证这个池化对象是否还可用(状态正常,可以被使用),
   * 如果验证失败,则对象销毁方法(destroyObject)会被触发。
   * 特别注意:
   * 1、在创建新的池化对象时,且设置了TestOnCreate为true时,该方法才会被触发
   * 2、在借出池化对象时,且设置了TestOnBorrow为true时,该方法才会被触发
   * 3、在归还池化对象时,且设置了TestOnReturn为true时,该方法才会被触发
   */
  @Override
  public boolean validateObject(PooledObject<MyObject> p) {
    MyObject myObject = p.getObject();
    boolean validate = myObject.validate();
    System.out.println(String.format("**** 验证一个正在使用的对象 %s , 验证结果为 %b" , myObject.getUuid() , validate));
    return validate;
  }
  /**
   * 当对象池被要求借出池化对象时,该方法一定会被触发。在正式借出前,可以修改池化对象的一些业务状态,做一些准备工作。
   * 注意:</br>
   * 1、该方法如果抛出异常,则这个将要被借出的对象将被销毁。</br>
   * 2、该方法如果抛出异常,且这个将要被借出的对象是一个刚被创建的对象,则这个对象将被销毁且异常会被抛出</br>
   */
  @Override
  public void activateObject(PooledObject<MyObject> p) throws Exception {
    
  }
  /**
   * 当池化对象被归还时,该方法一定会被触发。在完成归还动作前,操作者可以在这个方法中完成一些业务状态操作,做一些“擦屁股”性质的工作
   * 注意:如果该方法抛出异常,则这个被刚归还还没有正式回归对象池的对象,将被销毁
   */
  @Override
  public void passivateObject(PooledObject<MyObject> p) throws Exception {
    
  }
  /**
   * 当对象池需要销毁当前池化对象时,该方法被触发。
   * 触发原因可能是多种的,可能是已经到了设置的最大借出数量,也可能是validateObject方法返回为false,
   * 也可能是activateObject方法或者passivateObject方法抛出了异常
   */
  @Override
  public void destroyObject(PooledObject<MyObject> p) throws Exception {
    MyObject myObject = p.getObject();
    try {
      String uuid = myObject.getUuid();
      Integer count =  myObject.count();
      System.out.println(String.format("XXXXXXX 销毁一个不能使用的对象 %s , 销毁时对象被使用了%s 次" ,uuid , count.toString()));
    } catch(Exception e) {
      e.printStackTrace(System.out);
    }
    // 进行对象内部的关闭操作
    myObject.close();
    super.destroyObject(p);
  }
}

3.4、进行对象池的使用

在这个示例中,我们创建一个线程池并在线程池的工作中,只用这个对象池。创建线程池是为了保证应用程序在多线程并行处理过程上和线程数量上能有一个性能平衡点。

public class App {
  
  public static void main(String[] args) {
    MyObjectFactory factory = new MyObjectFactory();
    GenericObjectPoolConfig<MyObject> config = new GenericObjectPoolConfig<MyObject>();
    config.setMaxTotal(2000);
    config.setMaxIdle(10);
    config.setMinIdle(5);
    // ...... 补充其它的
    // 当对象池中的池化对象耗尽,调用者是否阻塞等待执行,直到能过去到对象
    config.setBlockWhenExhausted(true);
    // 设置池化对象在创建、借出、归还时,是否验证池化对象的可用性
    config.setTestOnBorrow(false);
    config.setTestOnCreate(false);
    config.setTestOnReturn(true);
    // 池中空闲的对象,最早等待多久被回收(1分钟)
    config.setMinEvictableIdleTimeMillis(60000l);
    // 该池是否可以进行JMX监控
    config.setJmxEnabled(true);
    
    // 创建对象池
    final GenericObjectPool<MyObject> pool = new GenericObjectPool<MyObject>(factory, config);
    // 使用一个线程池,不停的调用
    LinkedBlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>();
    ThreadPoolExecutor execute = new ThreadPoolExecutor(15, 30, 10l, TimeUnit.SECONDS, workQueue);
    for(int index = 0 ; index < 1000 ; index++) {
      execute.submit(new ExecuteTask(pool , index));
    }
  }
  
  // 执行任务
  private static class ExecuteTask implements Runnable {
    private GenericObjectPool<MyObject> pool;
    // 记录这是第多少个任务
    private int index;
    
    public ExecuteTask(GenericObjectPool<MyObject> pool , int index) {
      this.pool = pool;
      this.index = index;
    }
    
    @Override
    public void run() {
      // 取出对象,进行使用,并一定要还回
      MyObject myObject = null;
      try {
        myObject = this.pool.borrowObject();
        myObject.doSomething();
      } catch(Exception e) {
        e.printStackTrace(System.out);
      } finally {
        if(myObject != null) {
          // 这个20%的随机情况,模拟一部分使用者在使用过程中,主动关闭了对象
          Random random = new Random();
          if(random.nextFloat() <= 0.2f) {
            this.doClose(myObject);
          }
          this.pool.returnObject(myObject);
        }
        System.out.println(String.format("  ------- 第%d个任务完成工作  ------- " , this.index));
      }
    }
    
    private void doClose(MyObject myObject) {
      try {
        myObject.close();
      } catch (IOException e) {
        e.printStackTrace(System.out);
      }
    }
  }
}

注意:实际工作中,一定使用和对象池配套的线程池,才能达到提高性能的目的。 另外,经常看笔者文章的读者都知道,在正式工作中不能使用System.out进行输出。而这里之所以这样用,完全是为了简化依赖包的引入。

  • 这里对GenericObjectPoolConfig所提供的的配置信息含义进行一个简要介绍:
配置信息配置信息含义
maxTotal对象池中可容纳的最大对象数。
maxIdle对象池中允许存在的最大闲置对象数。
minIdle对象池中允许存在的最小闲置对象数。注意:maxTotal、maxIdle、minIdle三者的关系,还需要读者知晓对象池运行的过程。
blockWhenExhausted当对象池中的池化对象耗尽,调用者是否阻塞等待执行,直到能获取到对象。
testOnCreate在建立对象时检测对象是否有效(默认为true) , 一些资料提到配置true会导致性能下降,纯粹乱说,这只是检测时机的问题。
testOnBorrow在从对象池获取对象时是否检测对象有效(默认为true) ,一些资料提到配置true会导致性能下降,纯粹乱说,这只是检测时机的问题。
testOnReturn在向对象池中归还对象时是否检测对象有效(默认为true) ,一些资料提到配置true会导致性能下降,纯粹乱说,这只是检测时机的问题。根据实际情况笔者个人偏向在onReturn的事件发生时,进行对象有效性校验。
testWhileIdle在对象池中的对象空闲下来后,是否检测对象的有效性(默认为false)。如果开发人员使用对象池所管理的对象本身存在一个比较复杂的检测逻辑,来判定对象是否失效,那么可以将该属性设置为true。这样对象池会专门有一个“闲置检测线程”来独立进行对象是否有效的检测。
timeBetweenEvictionRunsMillis如果设置testWhileIdle为true,该属性则设定“闲置检测线程”对对象池中的闲置对象的检测周期。
minEvictableIdleTimeMillis对象池中的闲置对象最小空闲时间,达到此值后该空闲对象可能会被移除。不过是否要移除,还需看是否已达设定的最大空闲数(示例中设置的是1分钟)
jmxEnabled是否打开对象池的JMX监控,相关的属性还有:jmxNamePrefix、jmxNameBase
  • 下面对使用过程中的常用调用方法进行介绍:在对象池被创建后,GenericObjectPool对象就是开发人员直接调用的对象了,其中主要调用的方法包括:
方法名方法介绍
addObject调用该方法一般用于初始化对象池,给对象池添加一个对象。如果对象池已满,则什么操作都不做。在多数场景下,该方法实际上不用被显示调用,除非使用者对于该对象池的使用有很强的性能方面的考虑。
borrowObject从对象池取得一个对象,该方法用注意两个细节——a、blockWhenExhausted=false,如果对象池中的对象数量已经达到maxTotal设定的数量,立即抛出NoSuchElementException异常——b、blockWhenExhausted=true,如果对象池中的对象数量已经达到maxTotal设定的数量,会等待maxWaitMillis毫秒,之后还是没有获取到对象则抛出NoSuchElementException异常。
returnObject归还一个对象到对象池。
invalidateObject从对象池删除一个对象,如果有线程在等待对象,则会再创建一个对象。
getNumIdle返回空闲对象个数。
getNumActive返回被使用的对象个数。
clear清除所有的空闲对象,释放闲置资源。
close关闭对象池,释放对象池所有资源,close之后borrowObject会产生异常,returnObject不会产生异常。

以下是示例程序可能的运行结果:

生成一个新的对象 : d50850bb-491b-4558-92dc-2e49c8cecefd 
生成一个新的对象 : 77bb46f7-3254-4310-9298-afb025edecab 
生成一个新的对象 : 08bf9e4d-c0c1-4d78-a64c-5eb8b1c79dc7 
生成一个新的对象 : dce1d2ea-0adb-42db-8ad0-96356799554d 
生成一个新的对象 : d6d1e75f-0d63-45e8-b3a5-3396f980a8c3 
生成一个新的对象 : b8e46e6f-58fb-4006-970e-c15fe10d1e36 
生成一个新的对象 : 2c9608ff-d1fb-4b13-a967-c3cad67a957f 
生成一个新的对象 : 2fdc5b30-8376-4d02-9b9c-63da0e2e6f35 
生成一个新的对象 : 3340d781-3c75-42c9-add2-725b09814870 
生成一个新的对象 : ff4a8f65-7d4f-4ada-bbad-20c6dff08784 
生成一个新的对象 : b08d2a59-a8e2-4bdd-bf28-2fc9bf239333 
生成一个新的对象 : 1d94099b-ae7a-4beb-b291-3eeb74576454 
生成一个新的对象 : 7c04de5f-e82e-4b29-a4c7-41eb2b273294 
生成一个新的对象 : 2f5fab6e-287c-4416-938b-6132c030e371 
生成一个新的对象 : 21122161-f8f9-421b-ad3b-6eabc5aadff3 
...... 模拟工作过程 ...... 对象:2f5fab6e-287c-4416-938b-6132c030e371,进行了工作 ......
...... 模拟工作过程 ...... 对象:d6d1e75f-0d63-45e8-b3a5-3396f980a8c3,进行了工作 ......
...... 模拟工作过程 ...... 对象:3340d781-3c75-42c9-add2-725b09814870,进行了工作 ......
.......... 省略日志内容 ..........
...... 模拟工作过程 ...... 对象:d50850bb-491b-4558-92dc-2e49c8cecefd,进行了工作 ......
...... 模拟工作过程 ...... 对象:21122161-f8f9-421b-ad3b-6eabc5aadff3,进行了工作 ......
**** 验证一个正在使用的对象 7c04de5f-e82e-4b29-a4c7-41eb2b273294 , 验证结果为 true
**** 验证一个正在使用的对象 b08d2a59-a8e2-4bdd-bf28-2fc9bf239333 , 验证结果为 false
**** 验证一个正在使用的对象 2fdc5b30-8376-4d02-9b9c-63da0e2e6f35 , 验证结果为 true
**** 验证一个正在使用的对象 21122161-f8f9-421b-ad3b-6eabc5aadff3 , 验证结果为 true
XXXXXXX 销毁一个不能使用的对象 b08d2a59-a8e2-4bdd-bf28-2fc9bf239333 , 销毁时对象被使用了1 次
  ------- 第10个任务完成工作  ------- 
  ------- 第7个任务完成工作  ------- 
  ------- 第12个任务完成工作  ------- 
  ------- 第13个任务完成工作  ------- 
...... 模拟工作过程 ...... 对象:2fdc5b30-8376-4d02-9b9c-63da0e2e6f35,进行了工作 ......
...... 模拟工作过程 ...... 对象:7c04de5f-e82e-4b29-a4c7-41eb2b273294,进行了工作 ......
...... 模拟工作过程 ...... 对象:21122161-f8f9-421b-ad3b-6eabc5aadff3,进行了工作 ......
生成一个新的对象 : 03195a9b-c651-47a6-b905-4b87b8af915a 
...... 模拟工作过程 ...... 对象:03195a9b-c651-47a6-b905-4b87b8af915a,进行了工作 ......
**** 验证一个正在使用的对象 2c9608ff-d1fb-4b13-a967-c3cad67a957f , 验证结果为 true
**** 验证一个正在使用的对象 b8e46e6f-58fb-4006-970e-c15fe10d1e36 , 验证结果为 false
  ------- 第1个任务完成工作  ------- 
**** 验证一个正在使用的对象 08bf9e4d-c0c1-4d78-a64c-5eb8b1c79dc7 , 验证结果为 true
XXXXXXX 销毁一个不能使用的对象 b8e46e6f-58fb-4006-970e-c15fe10d1e36 , 销毁时对象被使用了1 次
**** 验证一个正在使用的对象 ff4a8f65-7d4f-4ada-bbad-20c6dff08784 , 验证结果为 true
  ------- 第3个任务完成工作  ------- 
  ------- 第9个任务完成工作  ------- 
...... 模拟工作过程 ...... 对象:2c9608ff-d1fb-4b13-a967-c3cad67a957f,进行了工作 ......
  ------- 第2个任务完成工作  ------- 
...... 模拟工作过程 ...... 对象:ff4a8f65-7d4f-4ada-bbad-20c6dff08784,进行了工作 ......
...... 模拟工作过程 ...... 对象:08bf9e4d-c0c1-4d78-a64c-5eb8b1c79dc7,进行了工作 ......
生成一个新的对象 : 6b798bf7-1a91-40c8-b035-90756f086795 
...... 模拟工作过程 ...... 对象:6b798bf7-1a91-40c8-b035-90756f086795,进行了工作 ......

.......... 省略日志内容 ..........

后续有时间我们再来详细说明对象池的内部运行过程。

文章来源:https://blog.csdn.net/yinwenjie/article/details/135319549
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。