当前位置: 首页 > 工具软件 > 豆豆Pool > 使用案例 >

对象池(连接池):commons-pool2源码解析:GenericObjectPool的borrowObject方法

宗涵蓄
2023-12-01

为什么会有对象池

在实际的应用工程当中,存在一些被频繁使用的、创建或者销毁比较耗时、持有的资源也比较昂贵的一些对象。比如:数据库连接对象、线程对象。所以如果能够通过一种方式,把这类对象统一管理,让这类对象可以被循环利用的话,就可以减少很多系统开销(内存、CPU、IO等),极大的提升应用性能。

Apache Commons Pool

Apache Commons Pool就是一个对象池的框架,他提供了一整套用于实现对象池化的API,以及若干种各具特色的对象池实现。Apache Commons Pool是很多连接池实现的基础,比如DBCP连接池、Jedis连接池等。
Apache Commons Pool有个两个大版本,commons-pool和commons-pool2。commons-pool2是对commons-pool的重构,里面大部分核心逻辑实现都是完全重写的。我们所有的源码分析都是基于commons-pool2。

在commons-pool2中,对象池的核心接口叫做ObjectPool,他定义了对象池的应该实现的行为。

  • addObject方法:往池中添加一个对象。池子里的所有对象都是通过这个方法进来的。
  • borrowObject方法:从池中借走到一个对象。借走不等于删除。对象一直都属于池子,只是状态的变化。
  • returnObject方法:把对象归还给对象池。归还不等于添加。对象一直都属于池子,只是状态的变化。
  • invalidateObject:销毁一个对象。这个方法才会将对象从池子中删除,当然这其中最重要的就是释放对象本身持有的各种资源。
  • getNumIdle:返回对象池中有多少对象是空闲的,也就是能够被借走的对象的数量。
  • getNumActive:返回对象池中有对象对象是活跃的,也就是已经被借走的,在使用中的对象的数量。
  • clear:清理对象池。注意是清理不是清空,改方法要求的是,清理所有空闲对象,释放相关资源。
  • close:关闭对象池。这个方法可以达到清空的效果,清理所有对象以及相关资源。

在commons-pool2中,ObjectPool的核心实现类是GenericObjectPool。

在前面的文章中我已经解析过addObject方法的实现,对应链接地址:

  • addObject方法解析:https://blog.csdn.net/weixin_42340670/article/details/107749058

本文解析来解析borrowObject方法在GenericObjectPool中的实现。

在讨论具体实现之前,我们还有必要看一下该方法在ObjectPool接口的具体定义是如何描述的。

ObjectPool接口中borrowObject解析

ObjectPool是对象池的顶层接口,既然是接口,那肯定是只声明方法,不实现。那我们解析的是什么,当然就是方法注释。

/**
    * Obtains an instance from this pool.
    * <p>
    * Instances returned from this method will have been either newly created
    * with {@link PooledObjectFactory#makeObject} or will be a previously
    * idle object and have been activated with
    * {@link PooledObjectFactory#activateObject} and then validated with
    * {@link PooledObjectFactory#validateObject}.
    * <p>
    * By contract, clients <strong>must</strong> return the borrowed instance
    * using {@link #returnObject}, {@link #invalidateObject}, or a related
    * method as defined in an implementation or sub-interface.
    * <p>
    * The behaviour of this method when the pool has been exhausted
    * is not strictly specified (although it may be specified by
    * implementations).
    *
    * @return an instance from this pool.
    *
    * @throws IllegalStateException
    *              after {@link #close close} has been called on this pool.
    * @throws Exception
    *              when {@link PooledObjectFactory#makeObject} throws an
    *              exception.
    * @throws NoSuchElementException
    *              when the pool is exhausted and cannot or will not return
    *              another instance.
    */
/**
    * 从池子里获取出一个实例。
    * 从池里返回的实例既可能是一个新创建的对象,也可能是之前创建的但是当前未被使用的空闲对象。
    * 返回的对象将会通过PooledObjectFactory的activateObject方法进行激活,然后通过PooledObjectFactory的validateObject方法进行校验有效性。
    * 根据约定,客户端必须通过调用returnObject(正常归还)、invalidateObject(销毁实例)来达到归还实例的目的。或者(约定之外)通过定义在子类中的相关实现方法进行归还。
    * 这个方法并没有强制规定当对象池耗尽的时候到底应该如何处理(是抛异常呢,还是无限等待呢,还是返回空呢),子类可以有自定义实现。

    * @return 返回值就是从对象池里获取到的实例
    * @throws IllegalStateException 当对象池已经被关闭(调用过close方法),就会抛出IllegalStateException异常。
    * @throws Exception 对象池里没有空闲对象,就需要通过PooledObjectFactory的makeObject方法来创建一个新的实例,如果创建过程中发生异常,则会抛出Exception异常。
    * @throws NoSuchElementException 当对象池被耗尽,不能正常返回对象的时候,抛出NoSuchElementException异常。(结合上面的描述,此处这个异常定义只是建议性的,子类实现时完全可以不抛出。)
*/    
T borrowObject() throws Exception, NoSuchElementException,
        IllegalStateException;

当我们把方法注释解析完之后,就能够对方法的用途,以及部分实现逻辑有一个大概的了解。
接下来我们再解析具体的实现。

GenericObjectPool类中borrowObject解析

/**
 * Equivalent to <code>{@link #borrowObject(long)
 * borrowObject}({@link #getMaxWaitMillis()})</code>.
 * <p>
 * {@inheritDoc}
 */
/**
    * 这就是实现了ObjectPool接口的borrowObject
    * 但是内部调用了自己的一个重载的,支持最大等待时间的方法。
 */
@Override
public T borrowObject() throws Exception {
    return borrowObject(getMaxWaitMillis());
}

/**
    * Borrow an object from the pool using the specific waiting time which only
    * applies if {@link #getBlockWhenExhausted()} is true.
    * <p>
    * If there is one or more idle instance available in the pool, then an
    * idle instance will be selected based on the value of {@link #getLifo()},
    * activated and returned. If activation fails, or {@link #getTestOnBorrow()
    * testOnBorrow} is set to <code>true</code> and validation fails, the
    * instance is destroyed and the next available instance is examined. This
    * continues until either a valid instance is returned or there are no more
    * idle instances available.
    * <p>
    * If there are no idle instances available in the pool, behavior depends on
    * the {@link #getMaxTotal() maxTotal}, (if applicable)
    * {@link #getBlockWhenExhausted()} and the value passed in to the
    * <code>borrowMaxWaitMillis</code> parameter. If the number of instances
    * checked out from the pool is less than <code>maxTotal,</code> a new
    * instance is created, activated and (if applicable) validated and returned
    * to the caller. If validation fails, a <code>NoSuchElementException</code>
    * is thrown.
    * <p>
    * If the pool is exhausted (no available idle instances and no capacity to
    * create new ones), this method will either block (if
    * {@link #getBlockWhenExhausted()} is true) or throw a
    * <code>NoSuchElementException</code> (if
    * {@link #getBlockWhenExhausted()} is false). The length of time that this
    * method will block when {@link #getBlockWhenExhausted()} is true is
    * determined by the value passed in to the <code>borrowMaxWaitMillis</code>
    * parameter.
    * <p>
    * When the pool is exhausted, multiple calling threads may be
    * simultaneously blocked waiting for instances to become available. A
    * "fairness" algorithm has been implemented to ensure that threads receive
    * available instances in request arrival order.
    *
    * @param borrowMaxWaitMillis The time to wait in milliseconds for an object
    *                            to become available
    *
    * @return object instance from the pool
    *
    * @throws NoSuchElementException if an instance cannot be returned
    *
    * @throws Exception if an object instance cannot be returned due to an
    *                   error
    */
/**
    * 这个方法是从池里获取一个对象,重点是支持一个等待时间设置。
    * 当getBlockWhenExhausted方法返回true的时候,意味着等待时间会生效;否则的话即使设置了等待时间,也不会生效。
    * 
    * 如果池子里存在着空闲实例,那么按照后进先出原则返回一个实例,然后激活返回的实例。
    * 如果激活实例失败,或者在testOnBorrow配置为true的情况下进行有效性校验的时候失败,那么都需要销毁这个实例,
    * 然后再取一下个,重复以上步骤,直到获取到了一个有效的实例或者没有空闲实例了。
    
    * 如果池子里没有空闲实例了,那么要怎么做就得取决于以下几个变量了
    * 1、getMaxTotal返回的最大限制
    * 2、getBlockWhenExhausted返回的在池满的时候是否阻塞的标识
    * 3、borrowMaxWaitMillis 通过方法参数传进来的阻塞时间
    * 如果说我们检查池里的实例数量小于maxTotal,那么说明所有实例都被占用,就需要创建一个新的实例。同样也需要经过激活、
    * 有效性校验(如果testOnBorrow配置为true)通过后返回给调用方。如果校验失败,则抛出NoSuchElementException异常。
    * 如果没有空闲实例(就说明资源都被占用)而且实例数也达到了maxTotal(不允许再创建新实例),就认为池子里的资源耗尽了。
    * 这个时候如果getBlockWhenExhausted返回true,说明允许阻塞等待一会,也许能等到有实例被释放,等待的时长就是通过参数传进来的borrowMaxWaitMillis。
    * 如果getBlockWhenExhausted返回false,那就直接抛出NoSuchElementException异常。
    * 当池子资源耗尽的时候,多个调用线程可能都在阻塞等待着有资源被释放,通过一个公平的算法,保证先来的请求线程先获取到释放的资源。
    * 
    * @param borrowMaxWaitMillis 池子资源耗尽的时候,为了获取可用实例,等待多长时间
    * @return 返回获取到的实例
    * @throws NoSuchElementException 这个异常可以认为是因为合理原因导致不能返回实例的时候抛出的。(比如激活失败、校验不通过、池子满了资源不释放)
    * @throws Exception 这个异常可以认为是意料之外的(遇到这个不太容易,下面源码解析的时候再细说)

*/    
public T borrowObject(long borrowMaxWaitMillis) throws Exception {

    /*
        这一步就是检查一下对象池状态,看看是否已经被关闭了,实际上就是判断内部的isClosed属性
        在ObjectPool接口的borrowObject方法注释中有提到,如果被关闭了,那么会抛出IllegalStateException
    */
    assertOpen(); 
    

    // 这里面的逻辑是检查是否需要移除已经被废弃的实例。
    // 关于abandonedConfig的解析可参见:https://blog.csdn.net/weixin_42340670/article/details/107136994
    AbandonedConfig ac = this.abandonedConfig;
    if (ac != null && ac.getRemoveAbandonedOnBorrow() &&
            (getNumIdle() < 2) &&
            (getNumActive() > getMaxTotal() - 3) ) {
        removeAbandoned(ac);
    }

    // 定义一个实例包装对象,他里面的T才是真正要返回的实例,总之只要他指向了具体对象那么就可以认为能够得到想要的实例
    PooledObject<T> p = null; 

    // Get local copy of current config so it is consistent for entire
    // method execution
    /*
        这行代码很好理解,就是取出是否阻塞的标识。但是重点在上面的英文注释。
        取出当前池子的这个是否阻塞的值,赋值给我方法内的一个局部变量,这样别的线程再改变这个标识也不会影响到我的方法。
        什么意思?
        getBlockWhenExhausted方法返回就是一个实例属性blockWhenExhausted(此处方法中的局部变量和实例变量同名)
        这个blockWhenExhausted属性定义在BaseGenericObjectPool抽象类中,默认为ture
        这个blockWhenExhausted属性是对象池的实例属性,我们的对象池只有一个,但是使用对象池的调用线程可能有多个。
        一旦其他线程通过setBlockWhenExhausted方法更改了blockWhenExhausted的值,就会影响到所有其他线程。
        所以这里拷贝出来一个局部变量,杜绝这种干扰。这就是一种线程安全的编程手段。
    */
    boolean blockWhenExhausted = getBlockWhenExhausted();

    boolean create; // 用来标识得到的实例是否是新创建的
    long waitTime = System.currentTimeMillis();  // 定义一个开始时间,后续是用来校验borrowMaxWaitMillis是否超时

    while (p == null) { // 这是一个循环,如果p为空,就说明还没有取到实例
        /*
        上面只定义了没有赋初值,但是默认值就应该为false。为啥这还要设置false? 
        因为经过循环之后,有可能经历过对象的创建,但是校验没有通过,也就没有被成功返回。
        再经历新一轮的循环的时候,可能就已经有空闲的了,那么有空闲的肯定就不需要再创建,所以这个标识也要重置为false。
        */
        create = false; 
        if (blockWhenExhausted) { // 如果设置了对象池耗尽后阻塞等待的标识
            p = idleObjects.pollFirst(); // 首先从空闲对象集合里弹出第一个元素(后进先出),这个方法是非阻塞的,也就是有空闲就返回,没有就返回空
            if (p == null) { // 如果弹出的元素为空,说明没有空闲
                p = create(); // 需要创建一个新的对象
                if (p != null) { // 如果p不能空,说明创建成功
                    create = true; // 创建标识置为true
                }
            }
            if (p == null) { // 如果这里为真,说明create没有创建成功,那么p就仍然为空。
                if (borrowMaxWaitMillis < 0) {  // 如果未设置 或者 设置了一个无效的等待时间
                    p = idleObjects.takeFirst(); // 仍然是从空闲队列获取,但是该方法会阻塞,一直等到有可用空闲对象。
                } else { // 如果设置了一个有效的等待时间
                    p = idleObjects.pollFirst(borrowMaxWaitMillis,
                            TimeUnit.MILLISECONDS); // 仍然是从空闲队列获取,但是该方法只最多等待borrowMaxWaitMillis毫秒。还取不到就返回空。
                }
            }
            if (p == null) { // 如果此时p还是为空,就说明没等待空闲,所以抛出一个合适的“等待空闲对象超时异常”
                throw new NoSuchElementException(
                        "Timeout waiting for idle object");
            }

            /*
            能走到这里,说明要么获取到了空闲对象,要么创建成功了新对象。
            p.allocate()从字面意思理解,是对象分配的意思,分配给谁?分配给调用方。
            这个方法是个同步方法,多线程安全,内部会校验并更改这个对象的一些状态和属性。
            如果对象的状态不为空闲状态,那么则返回false,意味着分配失败。
                为了什么状态会不为空闲状态? 因为存在多个请求方同时获取到了这个对象。
                因为allocate方法是加了synchronized的,一个请求方执行完allocate后,p的状态就不再空闲。其他请求方再执行allocate时就都返回false。
            */ 
            if (!p.allocate()) { // 如果分配失败(可认为被别人抢走了),p置为空(可以进行下一次循环遍历)
                p = null;
            }
        } else { // 如果没有设置无空闲等待
            p = idleObjects.pollFirst(); // 调用这个非阻塞方法获取空闲对象,有则返回对象,无则返回空
            if (p == null) {  // 如果弹出的元素为空,说明没有空闲
                p = create(); // 需要创建一个新的对象
                if (p != null) { // 如果p不能空,说明创建成功
                    create = true; // 创建标识置为true
                }
            }
            if (p == null) { // 如果此时p还是为空,直接抛出对象池耗尽异常
                throw new NoSuchElementException("Pool exhausted");
            }
            if (!p.allocate()) { // 如果分配失败(可认为被别人抢走了),p置为空(可以进行下一次循环遍历)
                p = null;
            }
        }

        /*
        通过上面的逻辑分析,我们在这里总结一下。
        走到这里
            如果p不为空,那么可以继续下面的激活和校验工作。
            如果p还为空,说明之前获取到了,但是被人抢走了(allocate返回了false),那么就继续下一次循环,再重新尝试获取。
        */

        if (p != null) { // 走到这里,说明获取到了,而且也分配成功了,p的状态已经变成了ALLOCATED状态(使用中状态)
            try {
                /*
                 通过对象池工厂,激活这个对象
                 在方法注释解析的部分我们也提到过,activateObject是PooledObjectFactory接口提供的一个抽象方法。
                 关于激活这件事,你用数据库连接池也好、redis连接池也好,应该都没有发现过和这个激活有关的自定义配置项。
                 我们最常干预的是否、何时进行validateObject。
                 激活这件事,个人理解:只框架的一个高层抽象,底层实现(dbcp连接池、jedis连接池)如果觉得有必要去做激活这件事,
                 那么你就去定义自己的实现逻辑,如果觉得没必要搞激活这个步骤,那给个空实现就可以了(activateObject这个方法是无返回值的)。
                 1、jedis连接池的PooledObjectFactory实现是JedisFactory,对于这个激活方法的实现是:做了一个redis的select连库请求。
                 2、dbcp连接池的PooledObjectFactory的实现是PoolableConnectionFactory,对于这个激活方法的实现是:设置数据库连接的autocommit、readonly等属性。
                */ 
                factory.activateObject(p);
            } catch (Exception e) {
                // 如果激活对象时,发生了异常
                try {
                    destroy(p); //需要销毁这个对象,释放资源
                } catch (Exception e1) {
                    // Ignore - activation failure is more important
                    // 销毁步骤已经是最后一步的收尾工作了,如果收尾还产生异常,那也没人再能给他收尾了,所以只能忽略掉
                }
                p = null; // destory之后,释放了资源,同时把p置为null,便于垃圾回收
                /*
                 校验一下这个被销毁的是不是新创建的对象,如果是新创建的对象
                 那么也就意味着,新创建了一个,分配成功了,但是激活失败了。
                 既然是新创建,也就意味着没有空闲的(也就意味着没必要再继续循环遍历了)。
                 没必要继续遍历,那么就抛出异常。
                */ 
                if (create) { 
                    NoSuchElementException nsee = new NoSuchElementException(
                            "Unable to activate object");
                    nsee.initCause(e);
                    throw nsee;
                }

                // 走到这里就说明,可以下一次循环,重新尝试获取
            }

            /*
             如果走到这里,有两种可能:
             p为空:说明获取到了空闲对象、但是激活失败了。 那么就可以进行下一次循环,重新获取。
             p不为空:说明激活也顺利通过了,接下来就看是否需要校验了。
                testOnBorrow属性为true 或者  是新创建的对象、并且testOnCreate属性为true
                这个逻辑也就意味着在配置testOnBorrow和testOnCreate的关系是
                    testOnBorrow如果设置为true,无论testOnCreate真假,无论是新创建对象还是空闲对象,则都会进行validate
                    testOnBorrow如果设置为fasle,那么只有testOnCreate为真,且确实是新创建的对象才会validate
            */ 
            if (p != null && (getTestOnBorrow() || create && getTestOnCreate())) {
                boolean validate = false; // 定义一个局部变量标识是否校验通过,默认false
                Throwable validationThrowable = null; // 定义一个局部变量存储校验时产生的异常
                try {
                    /*
                     调用工厂方法进行校验对象的有效性,校验失败则返回false
                     这个方法,不同的factory有不同的实现方式
                     大多数据库连接池(比如dbcp),默认会发一个select 1语句,校验连接的有效性。
                     jedis的实现是,发一条redis的ping命令来校验连接的有效性。
                     有的工厂实现捕捉到异常,会直接抛出;有的实现捕捉到异常会忽略,但是最终还是会返回false。
                    */ 
                    validate = factory.validateObject(p); 
                } catch (Throwable t) {
                    /*
                     检查捕捉到的异常是否需要抛出
                     这个方法内部实现比较简单,如果发现是ThreadDeath和VirtualMachineError两种Error则抛出
                     其他的Error和Exception则都忽略,但是也会被validationThrowable缓存起来,后续可能会抛出。
                    */ 
                    PoolUtils.checkRethrow(t); 
                    validationThrowable = t;
                }

                // 只要validate不为true,就说明校验失败了(比如:数据库后redis连接失效、网络通信异常等)
                if (!validate) {
                    try {
                        destroy(p); // 同样也要销毁该对象
                        destroyedByBorrowValidationCount.incrementAndGet(); // 递增一个因校验失败而销毁的次数
                    } catch (Exception e) {
                        // Ignore - validation failure is more important
                        // 销毁步骤已经是最后一步的收尾工作了,如果收尾还产生异常,那也没人再能给他收尾了,所以只能忽略掉
                    }
                    p = null; // 便于垃圾回收

                    /*
                    校验一下这个被销毁的是不是新创建的对象,如果是新创建的对象
                    那么也就意味着,新创建了一个,分配成功了,但是激活成功了,但是校验失败了。
                    既然是新创建,也就意味着没有空闲的(也就意味着没必要再继续循环遍历了)。
                    没必要继续遍历,那么就抛出异常。
                    */ 
                    if (create) {
                        NoSuchElementException nsee = new NoSuchElementException(
                                "Unable to validate object");
                        nsee.initCause(validationThrowable);
                        throw nsee;
                    }

                    // 走到这里就说明,可以下一次循环,重新尝试获取
                }
            }
        }
    }

    // 如果获取到了一个有效的对象,更新相关的统计信息。这个方法后续单独解析。
    updateStatsBorrow(p, System.currentTimeMillis() - waitTime);

    // 前面提到p是一个PooledObject实例,是一个包装对象,p.getObject()返回的才是调用方需要的对象。那PooledObject存在的意义是什么呢?我们后续单独解析。
    return p.getObject();
}
 类似资料: