池化技术(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<Worker> works ---保存当前所有的Worker(对thread的包装)
  • BlockingQueue<Runnable> 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<T> factory --- 用于创建新对象的工厂接口
  • LinkedBlockingDeque<PooledObject<T>> 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来进行对比分析会更好,这是本文后续需要补充的,之所以分析上面的三个组件是因为目前项目里面这几个用的比较多。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容