在实际的应用工程当中,存在一些被频繁使用的、创建或者销毁比较耗时、持有的资源也比较昂贵的一些对象。比如:数据库连接对象、线程对象。所以如果能够通过一种方式,把这类对象统一管理,让这类对象可以被循环利用的话,就可以减少很多系统开销(内存、CPU、IO等),极大的提升应用性能。
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,他定义了对象池的应该实现的行为。
在commons-pool2中,ObjectPool的核心实现类是GenericObjectPool。
在前面的文章中我已经解析过addObject方法的实现,对应链接地址:
- addObject方法解析:https://blog.csdn.net/weixin_42340670/article/details/107749058
本文解析来解析borrowObject方法在GenericObjectPool中的实现。
在讨论具体实现之前,我们还有必要看一下该方法在ObjectPool接口的具体定义是如何描述的。
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;
当我们把方法注释解析完之后,就能够对方法的用途,以及部分实现逻辑有一个大概的了解。
接下来我们再解析具体的实现。
/**
* 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();
}