簡介
池化技術(shù)能夠減少資源對象的創(chuàng)建次數(shù)问麸,提高程序的性能伙判,特別是在高并發(fā)下這種提高更加明顯。使用池化技術(shù)緩存的資源對象有如下共同特點:1衔憨,對象創(chuàng)建時間長叶圃;2,對象創(chuàng)建需要大量資源践图;3掺冠,對象創(chuàng)建后可被重復(fù)使用。下面介紹的thread码党,connection等對象都具有上面的幾個共同特點赫舒。本文通過jdk1.8的threadPool、jedis-client使用的apache-commons-pool2[2.4.2]闽瓢、以及數(shù)據(jù)庫連接池druid[1.1.10]等組件分析來感受下池化技術(shù)的使用接癌。
一個資源池具備如下功能:租用資源對象、歸還資源對象扣讼、清除過期資源對象缺猛,接下來我們就從這幾個功能點出發(fā)分別進行分析。
一 jdk1.8的ThreadPoolExecutor
線程池對外提供了一個任務(wù)提交入口execute(Runnable command)
椭符,這個接口接收任務(wù)并在內(nèi)部使用線程池來執(zhí)行荔燎。我們提交多個任務(wù)的時候,線程池使用多個線程同時執(zhí)行我們的任務(wù)销钝,下面主要分析線程池線程之間是如何組織來執(zhí)行我們的任務(wù)的有咨。
ThreadPoolExecutor的核心屬性和方法
- ThreadFactory threadFactory ---負責(zé)創(chuàng)建新的線程
- HashSet<Worker> works ---保存當(dāng)前所有的Worker(對thread的包裝)
- BlockingQueue<Runnable> workQueue ---(當(dāng)前的corePoolSize達到的時候,新提交的任務(wù)保存在這里)
- int corePoolSize ---核心Worker數(shù)量
- int maxPoolSize --- 最大的Worker數(shù)量
execute方法內(nèi)部主流程
當(dāng)前worker數(shù)量小于corePoolSize的時候創(chuàng)建新的線程并用這個線程執(zhí)行提交的任務(wù)
簡化代碼:
1 addWorker(command, true)
2 new Worker(firstTask) ;
3 workers.add(w);
4 t = w.thread;
5 t.start();
當(dāng)前worker數(shù)量大于等于corePoolSize的時候把任務(wù)添加到workQueue
workQueue.offer(command)
當(dāng)前worker數(shù)量超過了workQueue的capacity的時候創(chuàng)建新的線程并用這個線程執(zhí)行提交的任務(wù)
這里注意addWorker的第二個參數(shù)為false
1 addWorker(command, false)
內(nèi)部使用邏輯:
if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize)) return false;
2 new Worker(firstTask) ;
3 workers.add(w);
4 t = w.thread;
5 t.start();
當(dāng)前worker數(shù)量大于maxPoolSize的時候執(zhí)行拒絕策略:
reject(command);
線程重復(fù)利用以及回收
Worker核類心屬性及方法:
- Thread thread --- 用于執(zhí)行任務(wù)的線程
- Runnable firstTask --- 提交時候的任務(wù)
- Worker(Runnable firstTask) --- 創(chuàng)建一個Worker
- void run() --- 啟動thread執(zhí)行任務(wù)
Worker(Runnable firstTask)代碼片段
通過構(gòu)造方法調(diào)用threadFactory創(chuàng)建新的線程
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
run()代碼片段
直接調(diào)用ThreadPoolExecutor的runWorker(this)方法
1 while (task != null || (task = getTask()) != null) {
2 beforeExecute(wt, task);
3 task.run();
4 afterExecute(task, thrown); 線程執(zhí)行拋出的一些異常處理
}
5 processWorkerExit(w, completedAbruptly); 從works移除work
代碼1循環(huán)結(jié)束條件是沒有可執(zhí)行的任務(wù)即當(dāng)getTask()==null的時候蒸健,這時當(dāng)前線程的執(zhí)行也就結(jié)束了座享,等同于這個線程的回收;同時資源的重復(fù)利用點在于這里的循環(huán)似忧。代碼5主要是執(zhí)行了workers.remove(w)移除操作渣叛。為了保證池內(nèi)至少存在corePoolSize的work,在getTask()內(nèi)部判斷當(dāng)前workCount的數(shù)量,如果小于了coreSize盯捌,那么當(dāng)前線程會一直block淳衙,直到新的task到來。如果當(dāng)前workcount的數(shù)量超過了corePoolSize,并且workQueue為空饺著,表示這個線程不需要了箫攀,最多等待keepAliveTime的時間,也就是超過corePoolSize的線程最長存活的時間幼衰。
getTask()代碼片段如下:
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();
小結(jié)
從上面的流程可以發(fā)現(xiàn)線程池沒有顯示的調(diào)用歸還線程靴跛,因為線程是一直處于待執(zhí)行狀態(tài),只要有任務(wù)到來就立即執(zhí)行塑顺;對于線程的清理汤求,就是始終判斷當(dāng)前線程數(shù)量是否滿足預(yù)設(shè)的線程池數(shù)量俏险,如果超過就不再接收新的任務(wù)自然就退出了严拒。
二 commons-pool2
commons-pool2在很多客戶端被用于資源池的實現(xiàn)扬绪,jedis-client就是其中之一。commons-pool2和上面的threadPool不同裤唠,commons-pool2內(nèi)部的資源對象有不同的狀態(tài)挤牛,使用中、空閑等狀態(tài)种蘸,并且只有空閑狀態(tài)的資源對象是可以被申請使用的墓赴。
下面主要分析commons-pool2是如何提供池的功能的。
GenericObjectPool核心類屬性及方法:
- PooledObjectFactory<T> factory --- 用于創(chuàng)建新對象的工廠接口
- 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);
這個方法一般不是直接被調(diào)用的航瞭,而是框架(spring-data-redis)每次使用完了jedis連接后會調(diào)用jedis.close方法,這個方法進一步調(diào)用returnObject诫硕。
清除過期對象
BaseGenericObjectPool.Evictor類:
run() -> evict() -> destroy(underTest) -> idleObjects.remove(toDestory)
在設(shè)置timeBetweenEvictionRunsMillis的時候會開啟這個定時任務(wù)。
在jedis-client的使用代碼片段:
內(nèi)部維護一個GenericObjectPool對象來實現(xiàn)redis連接的復(fù)用
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);
}
小結(jié)
commons-pool2的結(jié)構(gòu)比較清晰刊侯,為資源對象的存儲回收都提供了很好的api章办,在一般的項目中接入使用很容易同時也非常高效。
三 druid
druid是一個數(shù)據(jù)庫連接池滨彻,池內(nèi)維護的是數(shù)據(jù)庫的連接藕届。druid并沒有使用commons-pool2這個框架,而是自己通過數(shù)組的方式實現(xiàn)了池的所有功能亭饵。下面從druid創(chuàng)建連接休偶、租用連接以及回收連接的過程分析池的所有功能。
DruidDataSource核心類屬性及方法:
- DruidConnectionHolder[] connections ---保存空閑的連接
- int poolingCount ---當(dāng)前池內(nèi)資源對象的計算
- DruidConnectionHolder[] evictConnections ---要被移除的連接
- ReentrantLock lock --- 重入鎖保證connections數(shù)組的安全訪問
- Condition notEmpty --- 空閑連接全部被使用等待其他客戶釋放鏈接辜羊,當(dāng)poolingCount為0的時候await,歸還連接的時候signal踏兜。
- Condition empty --- 創(chuàng)建連接的線程里面控制,當(dāng)poolingCount為0的時候signal創(chuàng)建連接八秃,當(dāng)前active的連接超過maxActive進行await庇麦。
- DruidPooledConnection getConnectionInternal(long maxWait) --- 獲取一個空閑連接
- recycle(DruidPooledConnection pooledConnection) --- 回收連接
創(chuàng)建連接
DruidDataSource:
init -> CreateConnectionThread.run -> createPhysicalConnection
從數(shù)組獲取一個空閑連接
DruidDataSource:
getConnection(long maxWaitMillis) -> getConnectionInternal(maxWaitMillis) -> pollLast(nanos) -> DruidConnectionHolder last = connections[poolingCount]
這里可以看到每次都是獲取數(shù)組的最后一個元素
歸還連接
DruidPooledConnection:
DruidPooledConnection 實現(xiàn)javax.sql.PooledConnection;這個方法在框架(mybatis)里面會執(zhí)行sql操作然后在finally代碼塊執(zhí)行javax.sql.PooledConnection.close()
close() -> recycle() -> dataSource.recycle(this) -> putLast(holder, lastActiveTimeMillis) -> connections[poolingCount] = e
關(guān)閉過期的連接
DruidDataSource:
在shrink方法內(nèi)部會判斷idleTime是否滿足條件
init -> createAndStartDestroyThread() -> run -> shrink(true, keepAlive) -> evictConnections[evictCount++] = connection -> close()
注意這里的close和上面歸還連接的close是不同的,這里是物理關(guān)閉
小結(jié)
druid沒有像commons-pool2一樣使用雙端隊列喜德,而是使用了數(shù)組;也沒有像commons-pool2一樣為對象設(shè)置多個不同的狀態(tài)山橄,druid使用兩個數(shù)組,一個用于存儲當(dāng)前可以使用的空閑連接舍悯,一個用于存儲要被清理的連接航棱。由于druid沒有使用雙端隊列,在并發(fā)不是很高的情況萌衬,數(shù)組里面最后一個連接被使用的頻率最高饮醇,極端情況只使用這一個,當(dāng)然這種情況對數(shù)據(jù)庫應(yīng)用沒有影響秕豫。
四 總結(jié)
從上面的三個組件可以看出資源對象的存儲使用了集合朴艰、雙端隊列观蓄、數(shù)組等數(shù)據(jù)結(jié)構(gòu);在面對資源競爭時使用鎖和Condition的組合來提高資源獲取的效率祠墅;當(dāng)資源不夠時侮穿,為客戶獲取資源對象提供了快速失敗、等待指定時間再失敗毁嗦,無限等待等方式亲茅。最后本文并沒有分析一些技術(shù)細節(jié),這不是本文的重點狗准。同時本文選擇的這幾個組件進行池化對比分析并不是最好克锣,比如分析數(shù)據(jù)庫連接池可以把druid、DBCP腔长、Tomcat-jdbc袭祟、C3P0來進行對比分析會更好,這是本文后續(xù)需要補充的捞附,之所以分析上面的三個組件是因為目前項目里面這幾個用的比較多巾乳。