java物件池commons-pool-1.6詳解(一)

趕路人兒發表於2017-12-23

物件的建立和銷燬在一定程度上會消耗系統的資源,雖然jvm的效能在近幾年已經得到了很大的提高,對於多數物件來說,沒有必要利用物件池技術來進行物件的建立和管理。但是對於有些物件來說,其建立的代價還是比較昂貴的,比如執行緒、tcp連線、rpc連線、資料庫連線等物件,因此物件池技術還是有其存在的意義。

Apache-commons-pool-1.6提供的物件池主要有兩種:一種是帶Key的物件池,這種帶Key的物件池是把相同的池物件放在同一個池中,也就是說有多少個key就有多少個池;另一種是不帶Key的物件池,這種物件池是把生產完全一致的物件放在同一個池中,但是有時候,單用對池內所有物件一視同仁的物件池,並不能解決的問題。例如:對於一組某些引數設定不同的同類物件——比如一堆指向不同地址的 java.net.URL物件或者一批代表不同語句的java.sql.PreparedStatement物件,用這樣的方法池化,就有可能取出不合用的物件。


1、物件池

1)物件池介面介紹:

如果讓我們去設計一個物件池介面,會給使用者提供哪些核心的方法呢?

borrowObject(),returnObject()是兩個核心方法,一個是’借’,一個是’還’。那麼我們有可能需要對一個已經借到的物件置為失效(比如當我們的遠端連線關閉或產生異常,這個連線不可用需要失效掉),invalidateObject()也是必不可少的。物件池剛剛建立的時候,我們可能需要預熱一部分物件,而不是採用懶載入模式以避免系統啟動時候的抖動,因此addObject()提供給使用者,以進行物件池的預熱。有建立就有銷燬,clear()和close()就是用來清空物件池(覺得叫purge()可能更好一點)。除此之外,我們可能還需要一些簡單的統計,比如getNumIdle()獲得空閒物件個數和getNumActive()獲得活動物件(被借出物件)的個數。如下表:

方法名 作用
borrowObject() 從池中借物件
returnObject() 還回池中
invalidateObject() 失效一個物件
addObject() 池中增加一個物件
clear() 清空物件池
close() 關閉物件池
getNumIdle() 獲得空閒物件數量
getNumActive() 獲得被借出物件數量
2)在commons-pool中有兩類物件池介面(帶key和不帶key),一個是ObjectPool,另一個是KeyedObjectPool;此外,為了方便他們分別還對應了ObjectPoolFactory、KeyedObjectPoolFactory兩個介面(這兩個介面在功能上和他們都一樣,只是使用形式上不一樣)

3)物件池空間劃分:

一個物件儲存到物件池中,其位置不是一成不變的。空間的劃分可以分為兩種,一種是物理空間劃分,一種是邏輯空間劃分。不同的實現可能採用不同的技術手段,Commons Pool實際上採用了邏輯劃分。如下圖所示:


從整體上來講,可以將空間分為池外空間和池內空間,池外空間是指被’出借’的物件所在的空間(邏輯空間)。池內空間進一步可以劃分為idle空間abandon空間invalid空間。idle空間就是空閒物件所在的空間,空閒物件之間是有一定的組織結構的(詳見後文)。abandon空間又被稱作放逐空間,用於放逐被出借超時的物件。invalid空間其實就是物件的垃圾場,這些物件將不會在被使用,而是等待被gc處理掉。

4)物件池的放逐與驅逐策略:

下面我們會多次提到驅逐(eviction)和放逐(abandon),這兩個概念是物件池設計的核心。

       先來看驅逐,我們知道物件池的一個重要的特性就是伸縮性,所謂伸縮性是指物件池能夠根據當前池中空閒物件的數量(maxIdle和minIdle配置)自動進行調整,進而避免記憶體的浪費。自動伸縮,這是驅逐所需要達到的目標,他是如何實現的呢?實際上在物件池內部,我們可以維護一個驅逐定時器(EvictionTimer),由timeBetweenEvictionRunsMillis引數對定時器的間隔加以控制,每次達到驅逐時間後,我們就選定一批物件(由numTestsPerEvictionRun引數進行控制)進行驅逐測試,這個測試可以採用策略模式,比如Commons Pool的DefaultEvictionPolicy,程式碼如下:

@Override
public boolean evict(EvictionConfig config, PooledObject<T> underTest,
        int idleCount) {
    if ((config.getIdleSoftEvictTime() < underTest.getIdleTimeMillis() &&
            config.getMinIdle() < idleCount) ||
            config.getIdleEvictTime() < underTest.getIdleTimeMillis()) {
        return true;
    }
    return false;
}
對於符合驅逐條件的物件,將會被物件池無情的驅逐出空閒空間,並丟棄到invalid空間。之後物件池還需要保證內部空閒物件數量需要至少達到minIdle的控制要求。

       我們在看來放逐,物件出借時間太長(由removeAbandonedTimeout控制),我們就把他們稱作流浪物件,這些物件很有可能是那些用完不還的壞蛋們的傑作,也有可能是物件使用者出現了什麼突發狀況,比如網路連線超時時間設定長於放逐時間。總之,被放逐的物件是不允許再次迴歸到物件池中的,他們會被擱置到abandon空間,進而進入invalid空間再被gc掉以完成他們的使命。放逐由removeAbandoned()方法實現,分為標記過程和放逐過程,程式碼實現並不難,有興趣的可以直接翻翻原始碼。

驅逐是由內而外將物件驅逐出境,放逐則是由外而內,將物件流放。他們一內一外,正是整個物件池形成閉環的核心要素。

5)物件池有效性探測:

用過資料庫連線池的同學可能對類似testOnBorrow的配置比較熟悉。除了testOnBorrow,物件池還提供了testOnCreate, testOnReturn, testWhileIdle,其中testWhileIdle是當物件處於空閒狀態的時候所進行的測試,當測試通過則繼續留在物件池中,如果失效,則棄置到invalid空間。所謂testOnBorrow其實就是當物件出借前進行測試,測試什麼?當然是有效性測試,在測試之前我們需要呼叫factory.activateObject()以啟用物件,在呼叫factory.validateObject(p)對準備出借的物件做有有效性檢查,如果這個物件無效則可能有丟擲異常的行為,或者返回空物件,這全看具體實現了。testOnCreate表示當物件建立之後,再進行有效性測試,這並不適用於頻繁建立和銷燬物件的物件池,他與testOnBorrow的行為類似。testOnReturn是在物件還回到池子之前鎖進行的測試,與出借的測試不同,testOnReturn無論是測試成功還是失敗,我們都需要保證池子中的物件數量是符合配置要求的()ensureIdle()方法就是做這個事情),並且如果測試失敗了,我們可以直接swallow這個異常,因為使用者根本不需要關心池子的狀態。

6)物件池的常見配置一覽:

配置引數 意義 預設值
maxTotal 物件總數 8
maxIdle 最大空閒物件數 8
minIdle 最小空閒物件書 0
lifo 物件池借還是否採用lifo true
fairness 對於借物件的執行緒阻塞恢復公平性 false
maxWaitMillis 借物件阻塞最大等待時間 -1
minEvictableIdleTimeMillis 最小驅逐空閒時間 30分鐘
numTestsPerEvictionRun 每次驅逐數量 3
testOnCreate 建立後有效性測試 false
testOnBorrow 出借前有效性測試 false
testOnReturn 還回前有效性測試 false
testWhileIdle 空閒有效性測試 false
timeBetweenEvictionRunsMillis 驅逐定時器週期 false
blockWhenExhausted 物件池耗盡是否block true

2、池化物件

1)池化物件介面:(被池化的物件需要實現該介面

池化物件就是物件池中所管理的基本單元。我們可以思考一下,如果直接將我們的原始物件放到物件池中是否可以?答案當然是可以,但是不好,因為如果那樣做,我們的物件池就退化成了容器Collection了,之所以需要將原始物件wrapper成池物件,是因為我們需要提供額外的管理功能,比如生命週期管理。commons pool採用了PooledObject<T>介面和KeyedPooledObject<T>介面用於表達池物件,它主要抽象了池物件的狀態管理和一些諸如狀態變遷時所產生的統計指標,這些指標可以配合物件池做更精準的管理操作。

2)池化物件狀態:

說到對池物件的管理,最重要的當屬對狀態的管理。對於狀態管理,我們熟知的模型就是狀態機模型了。池物件當然也有一套自己的狀態機,我們先來看看commons pool所定義的池物件都有哪些狀態:

狀態 解釋
IDLE 空閒狀態
ALLOCATED 已出借狀態
EVICTION 正在進行驅逐測試
EVICTION_RETURN_TO_HEAD 驅逐測試通過物件放回到頭部
VALIDATION 空閒校驗中
VALIDATION_PREALLOCATED 出借前校驗中
VALIDATION_RETURN_TO_HEAD 校驗通過後放回頭部
INVALID 無效物件
ABANDONED 放逐中
RETURNING 換回物件池中
這裡只需知道:放逐(ABANDONED)指的是不在物件池中的物件超時流放;驅逐(EVICTION)指的是空閒物件超時銷燬;VALIDATION是有效性校驗,主要校驗空閒物件的有效性。注意與驅逐和放逐之間的區別。我們通過一張圖來看看狀態之間的變遷。


我們看到上圖的’圓圈’表示的就是池物件,其中中間的英文簡寫是其對應的狀態。虛線外框則表示瞬時狀態。比如RETURNING和ABANDONED。這裡我們省略了VALIDATION_RETURN_TO_HEAD,VALIDATION_PREALLOCATED,EVICTION_RETURN_TO_HEAD,因為這對於我們理解池物件狀態變遷並沒有太多幫助。針對上圖,我們重點關注四個方面:

  1. IDLE->ALLOCATED 即上圖的borrow操作,除了需要將狀態置為已分配,我們還需要考慮如果物件池耗盡了怎麼辦?是繼續阻塞還是直接異常退出?如果阻塞是阻塞多久?
  2. ALLOCATED->IDLE 即上圖的return操作,我們需要考慮的是,如果池物件還回到物件池,此時物件池空閒數已經達到上界或該物件已經無效,我們是否需要進行特殊處理?
  3. IDLE->EVICTION 與 ALLOCATED->ABANDONED 請參考後文
  4. IDLE->VALIDATION 是testWhileIdle的有效性測試所需要經歷的狀態變遷,他是指每隔一段時間對池中所有的idle物件進行有效性檢查,以排除那些已經失效的物件。失效的物件將會棄置到invalid空間。

3)池化物件生命週期控制:

只搞清楚了池化物件的狀態和狀態轉移是不夠的,我們還應該能夠對池物件生命週期施加影響。Commons Pool通過PooledObjectFactory<T>介面和KeyedPooledObjectFactory<T>對物件生命週期進行控制。該介面有如下方法:

方法 解釋
makeObject 建立物件
destroyObject 銷燬物件
validateObject 校驗物件
activateObject 重新初始化物件
passivateObject 反初始化物件
我們需要注意,池物件必須經過建立(makeObject())和初始化過程(activateObject())後才能夠被我們使用。我們看一看這些方法能夠影響哪些狀態變遷。


4)池物件組織結構:

池中的物件,並不是雜亂無章的,他們得有一定的組織結構。不同的組織結構可能會從整體影響物件池的使用。Apache Commons提供了兩種組織結構,其一是有界阻塞雙端佇列(LinkedBlockingDeque),其二是key桶。


有界阻塞佇列能夠提供阻塞特性,當池中物件exhausted後,新申請物件的執行緒將會阻塞,這是典型的生產者/消費者模型,通過這種雙端的阻塞佇列,我們能夠實現池物件的lifo或fifo。如下程式碼:

if (getLifo()) {
    idleObjects.addFirst(p);
} else {
    idleObjects.addLast(p);
}
因為是帶有阻塞性質的佇列,我們能夠通過fairness引數控制執行緒獲得鎖的公平性,這裡我們可以參考AQS實現,不說了。下面我們再來看一看key桶的資料結構:




從上圖我們可以看出,每一個key對應一個的雙端阻塞佇列ObjectDeque,ObjectDeque實際上就是包裝了LinkedBlockingDeque,採用這種結構我們能夠對池物件進行一定的劃分,從而更加靈活的使用物件池。Commons Pool採用了KeyedObjectPool<K,V>用以表示採用這種資料結構的物件池。當我們borrow和return的時候,都需要指定對應的key空間。

參考:

http://kriszhang.com/object-pool/

物件池原始碼:https://github.com/apache/commons-pool

物件池wiki:https://commons.apache.org/proper/commons-pool/




相關文章