池化技术(JAVA)分析

谢柳平

简介

池化技术能够减少资源对象的创建次数,提高程序的性能,特别是在高并发下这种提高更加明显。使用池化技术缓存的资源对象有如下共同特点:1,对象创建时间长;2,对象创建需要大量资源;3,对象创建后可被重复使用。下面介绍的thread,connection等对象都具有上面的几个共同特点。本文通过jdk1.8的threadPool、jedis-client使用的apache-commons-pool2[2.4.2]、以及数据库连接池druid[1.1.10]等组件分析来感受下池化技术的使用。

一个资源池具备如下功能:租用资源对象、归还资源对象、清除过期资源对象,接下来我们就从这几个功能点出发分别进行分析。

一 jdk1.8的ThreadPoolExecutor

线程池对外提供了一个任务提交入口execute(Runnable command),这个接口接收任务并在内部使用线程池来执行。我们提交多个任务的时候,线程池使用多个线程同时执行我们的任务,下面主要分析线程池线程之间是如何组织来执行我们的任务的。

ThreadPoolExecutor的核心属性和方法

  • ThreadFactory threadFactory ---负责创建新的线程
  • HashSet works ---保存当前所有的Worker(对thread的包装)
  • BlockingQueue workQueue ---(当前的corePoolSize达到的时候,新提交的任务保存在这里)
  • int corePoolSize ---核心Worker数量
  • int maxPoolSize --- 最大的Worker数量
execute方法内部主流程

当前worker数量小于corePoolSize的时候创建新的线程并用这个线程执行提交的任务

简化代码:
1 addWorker(command, true)  
2 new Worker(firstTask) ;  
3 workers.add(w);  
4 t = w.thread;  
5 t.start();  

当前worker数量大于等于corePoolSize的时候把任务添加到workQueue

workQueue.offer(command)  

当前worker数量超过了workQueue的capacity的时候创建新的线程并用这个线程执行提交的任务

这里注意addWorker的第二个参数为false
1 addWorker(command, false)  
内部使用逻辑:
if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize)) return false;  
2 new Worker(firstTask) ;  
3 workers.add(w);  
4 t = w.thread;  
5 t.start();  

当前worker数量大于maxPoolSize的时候执行拒绝策略:

reject(command);  
线程重复利用以及回收

Worker核类心属性及方法:

  • Thread thread --- 用于执行任务的线程
  • Runnable firstTask --- 提交时候的任务
  • Worker(Runnable firstTask) --- 创建一个Worker
  • void run() --- 启动thread执行任务

Worker(Runnable firstTask)代码片段

通过构造方法调用threadFactory创建新的线程
Worker(Runnable firstTask) {  
      setState(-1); // inhibit interrupts until runWorker
      this.firstTask = firstTask;
      this.thread = getThreadFactory().newThread(this);
}

run()代码片段

直接调用ThreadPoolExecutor的runWorker(this)方法
1 while (task != null || (task = getTask()) != null) {  
2 beforeExecute(wt, task);  
3 task.run();  
4 afterExecute(task, thrown);  线程执行抛出的一些异常处理  
}
5 processWorkerExit(w, completedAbruptly); 从works移除work  

代码1循环结束条件是没有可执行的任务即当getTask()==null的时候,这时当前线程的执行也就结束了,等同于这个线程的回收;同时资源的重复利用点在于这里的循环。代码5主要是执行了workers.remove(w)移除操作。为了保证池内至少存在corePoolSize的work,在getTask()内部判断当前workCount的数量,如果小于了coreSize,那么当前线程会一直block,直到新的task到来。如果当前workcount的数量超过了corePoolSize,并且workQueue为空,表示这个线程不需要了,最多等待keepAliveTime的时间,也就是超过corePoolSize的线程最长存活的时间。

getTask()代码片段如下:

boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;  
Runnable r = timed ?  
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();  
小结

从上面的流程可以发现线程池没有显示的调用归还线程,因为线程是一直处于待执行状态,只要有任务到来就立即执行;对于线程的清理,就是始终判断当前线程数量是否满足预设的线程池数量,如果超过就不再接收新的任务自然就退出了。

二 commons-pool2

commons-pool2在很多客户端被用于资源池的实现,jedis-client就是其中之一。commons-pool2和上面的threadPool不同,commons-pool2内部的资源对象有不同的状态,使用中、空闲等状态,并且只有空闲状态的资源对象是可以被申请使用的。
下面主要分析commons-pool2是如何提供池的功能的。
GenericObjectPool核心类属性及方法:

  • PooledObjectFactory factory --- 用于创建新对象的工厂接口
  • LinkedBlockingDeque> idleObjects --- 保存空闲资源的双端队列
  • T borrowObject() ---租用对象
  • returnObject(T obj) ---归还对象

租用对象过程

1 T borrowObject() -> idleObjects.pollFirst()  
2 if(p == null) p = create() -> factory.makeObject()  

归还对象过程

void returnObject(T obj) -> idleObjects.addLast(p);  
这个方法一般不是直接被调用的,而是框架(spring-data-redis)每次使用完了jedis连接后会调用jedis.close方法,这个方法进一步调用returnObject。

清除过期对象

BaseGenericObjectPool.Evictor类:  
run() -> evict() -> destroy(underTest) -> idleObjects.remove(toDestory)  
在设置timeBetweenEvictionRunsMillis的时候会开启这个定时任务。
在jedis-client的使用代码片段:
内部维护一个GenericObjectPool对象来实现redis连接的复用
redis.clients.util.Pool类  
public void initPool(final GenericObjectPoolConfig poolConfig, PooledObjectFactory<T> factory) {  
    if (this.internalPool != null) {
      try {
        closeInternalPool();
      } catch (Exception e) {
      }
    }
    this.internalPool = new GenericObjectPool<T>(factory, poolConfig);
  }
小结

commons-pool2的结构比较清晰,为资源对象的存储回收都提供了很好的api,在一般的项目中接入使用很容易同时也非常高效。

三 druid

druid是一个数据库连接池,池内维护的是数据库的连接。druid并没有使用commons-pool2这个框架,而是自己通过数组的方式实现了池的所有功能。下面从druid创建连接、租用连接以及回收连接的过程分析池的所有功能。

DruidDataSource核心类属性及方法:

  • DruidConnectionHolder[] connections ---保存空闲的连接
  • int poolingCount ---当前池内资源对象的计算
  • DruidConnectionHolder[] evictConnections ---要被移除的连接
  • ReentrantLock lock --- 重入锁保证connections数组的安全访问
  • Condition notEmpty --- 空闲连接全部被使用等待其他客户释放链接,当poolingCount为0的时候await,归还连接的时候signal。
  • Condition empty --- 创建连接的线程里面控制,当poolingCount为0的时候signal创建连接,当前active的连接超过maxActive进行await。
  • DruidPooledConnection getConnectionInternal(long maxWait) --- 获取一个空闲连接
  • recycle(DruidPooledConnection pooledConnection) --- 回收连接

创建连接

DruidDataSource:  
init -> CreateConnectionThread.run -> createPhysicalConnection  

从数组获取一个空闲连接

DruidDataSource:  
getConnection(long maxWaitMillis) -> getConnectionInternal(maxWaitMillis) -> pollLast(nanos) -> DruidConnectionHolder last = connections[poolingCount]  
这里可以看到每次都是获取数组的最后一个元素

归还连接

DruidPooledConnection:  
DruidPooledConnection 实现javax.sql.PooledConnection;这个方法在框架(mybatis)里面会执行sql操作然后在finally代码块执行javax.sql.PooledConnection.close()  
close() ->  recycle() -> dataSource.recycle(this) -> putLast(holder, lastActiveTimeMillis) -> connections[poolingCount] = e  

关闭过期的连接

DruidDataSource:  
在shrink方法内部会判断idleTime是否满足条件
init -> createAndStartDestroyThread() -> run -> shrink(true, keepAlive) -> evictConnections[evictCount++] = connection -> close()  
注意这里的close和上面归还连接的close是不同的,这里是物理关闭
小结

druid没有像commons-pool2一样使用双端队列,而是使用了数组;也没有像commons-pool2一样为对象设置多个不同的状态,druid使用两个数组,一个用于存储当前可以使用的空闲连接,一个用于存储要被清理的连接。由于druid没有使用双端队列,在并发不是很高的情况,数组里面最后一个连接被使用的频率最高,极端情况只使用这一个,当然这种情况对数据库应用没有影响。

四 总结

从上面的三个组件可以看出资源对象的存储使用了集合、双端队列、数组等数据结构;在面对资源竞争时使用锁和Condition的组合来提高资源获取的效率;当资源不够时,为客户获取资源对象提供了快速失败、等待指定时间再失败,无限等待等方式。最后本文并没有分析一些技术细节,这不是本文的重点。同时本文选择的这几个组件进行池化对比分析并不是最好,比如分析数据库连接池可以把druid、DBCP、Tomcat-jdbc、C3P0来进行对比分析会更好,这是本文后续需要补充的,之所以分析上面的三个组件是因为目前项目里面这几个用的比较多。