使用Jakarta Commons Pool處理對象池化

2003 年 12 月
恰當地使用對象池化技術,可以有效地減少對象生成和初始化時的消耗,提高系統的運行效率。Jakarta Commons Pool組件提供了一整套用於實現對象池化的框架,以及若干種各具特色的對象池實現,可以有效地減少處理對象池化時的工作量,爲其它重要的工作留下更多的精力和時間。

創建新的對象並初始化的操作,可能會消耗很多的時間。在這種對象的初始化工作包含了一些費時的操作(例如,從一臺位於20,000千米以外的主機上讀出一些數據)的時候,尤其是這樣。在需要大量生成這樣的對象的時候,就可能會對性能造成一些不可忽略的影響。要緩解這個問題,除了選用更好的硬件和更棒的虛擬機以外,適當地採用一些能夠減少對象創建次數的編碼技巧,也是一種有效的對策。對象池化技術(Object Pooling)就是這方面的著名技巧,而Jakarta Commons Pool組件則是處理對象池化的得力外援。

對象池化技術
對象池化的基本思路是:將用過的對象保存起來,等下一次需要這種對象的時候,再拿出來重複使用,從而在一定程度上減少頻繁創建對象所造成的開銷。用於充當保存對象的“容器”的對象,被稱爲“對象池”(Object Pool,或簡稱Pool)。

對於沒有狀態的對象(例如String),在重複使用之前,無需進行任何處理;對於有狀態的對象(例如StringBuffer),在重複使用之前,就需要把它們恢復到等同於剛剛生成時的狀態。由於條件的限制,恢復某個對象的狀態的操作不可能實現了的話,就得把這個對象拋棄,改用新創建的實例了。

並非所有對象都適合拿來池化——因爲維護對象池也要造成一定開銷。對生成時開銷不大的對象進行池化,反而可能會出現“維護對象池的開銷”大於“生成新對象的開銷”,從而使性能降低的情況。但是對於生成時開銷可觀的對象,池化技術就是提高性能的有效策略了。

說明:英語中的Pool除了“池”之外,還有“供多方共享的資源”意思。作者十分懷疑第二種纔是“Object Pool”中的Pool的實際含義,但是“對象池”的說法已經廣爲流傳,而一時又沒有足以替代的貼切譯法,因此這裏仍然沿用這種譯名。

Jakarta Commons Pool組件

Jakarta Commons Pool是一個用於在Java程序中實現對象池化的組件。它的基本情況是:

  • 主要作者:Morgan Delagrange、Geir Magnusson、Craig McClanahan、Rodney Waldhoff、David Weinrich和Dirk Verbeeck

  • 最新版本:1.1

  • 所含包數:2個(org.apache.commons.pool和org.apache.commons.pool.impl)

  • 所含類數:21個(其中有4個抽象類和6個接口)

  • 適用平臺:Java 2, Standard Edition.

  • 單純地使用Pool組件不需要太多的Java 2的知識和經驗,對語法和基本概念(對象、異常、類、接口、實例、繼承和實現等)有一般瞭解即可。

下載和安裝
爲了順利的按照本文中提到的方法使用Pool組件,除去Java 2 SDK外,還需要先準備下列一些東西:

以上兩種軟件均有已編譯包和源代碼包兩種形式可供選擇。一般情況下,使用已編譯包即可。不過建議同時也下載源代碼包,作爲參考資料使用。

如果打算使用源代碼包自行編譯,那麼還需要準備以下一些東西:

具體的編譯方法,可以參看有關的Ant文檔。

將解壓或編譯後得到的commons-pool.jar和commons-collections.jar放入CLASSPATH,就可以開始使用Pool組件了。

PoolableObjectFactory、ObjectPool和ObjectPoolFactory
在Pool組件中,對象池化的工作被劃分給了三類對象:

  • PoolableObjectFactory用於管理被池化的對象的產生、激活、掛起、校驗和銷燬;

  • ObjectPool用於管理要被池化的對象的借出和歸還,並通知PoolableObjectFactory完成相應的工作;

  • ObjectPoolFactory則用於大量生成相同類型和設置的ObjectPool。

相應地,使用Pool組件的過程,也大體可以劃分成“創立PoolableObjectFactory”、“使用ObjectPool”和可選的“利用ObjectPoolFactory”三種動作。

創立PoolableObjectFactory
Pool組件利用PoolableObjectFactory來照看被池化的對象。ObjectPool的實例在需要處理被池化的對象的產生、激活、掛起、校驗和銷燬工作時,就會調用跟它關聯在一起的PoolableObjectFactory實例的相應方法來操作。

PoolableObjectFactory是在org.apache.commons.pool包中定義的一個接口。實際使用的時候需要利用這個接口的一個具體實現。Pool組件本身沒有包含任何一種PoolableObjectFactory實現,需要根據情況自行創立。

創立PoolableObjectFactory的大體步驟是:

  1. 創建一個實現了PoolableObjectFactory接口的類。

    
    import org.apache.commons.pool.PoolableObjectFactory;
    
    public class PoolableObjectFactorySample
            implements PoolableObjectFactory {
        private static int counter = 0;
    }
    
  2. 爲這個類添加一個Object makeObject()方法。這個方法用於在必要時產生新的對象。

    
    public Object makeObject() throws Exception {
        Object obj = String.valueOf(counter++);
        System.err.println("Making Object " + obj);
        return obj;
    }
    
    
  3. 爲這個類添加一個void activateObject(Object obj)方法。這個方法用於將對象“激活”——設置爲適合開始使用的狀態。

    
    public void activateObject(Object obj) throws Exception {
        System.err.println("Activating Object " + obj);
    }
    
    
  4. 爲這個類添加一個void passivateObject(Object obj)方法。這個方法用於將對象“掛起”——設置爲適合開始休眠的狀態。

    
    public void passivateObject(Object obj) throws Exception {
        System.err.println("Passivating Object " + obj);
    }
    
    
  5. 爲這個類添加一個boolean validateObject(Object obj)方法。這個方法用於校驗一個具體的對象是否仍然有效,已失效的對象會被自動交給destroyObject方法銷燬

    
    public boolean validateObject(Object obj) {
        boolean result = (Math.random() > 0.5);
        System.err.println("Validating Object "
                + obj + " : " + result);
        return result;
    }
    
  6. 爲這個類添加一個void destroyObject(Object obj)方法。這個方法用於銷燬被validateObject判定爲已失效的對象。

    
    public void destroyObject(Object obj) throws Exception {
        System.err.println("Destroying Object " + obj);
    }
    

最後完成的PoolableObjectFactory類似這個樣子:


PoolableObjectFactorySample.java

import org.apache.commons.pool.PoolableObjectFactory;

public class PoolableObjectFactorySample
        implements PoolableObjectFactory {
    private static int counter = 0;

    public Object makeObject() throws Exception {
        Object obj = String.valueOf(counter++);
        System.err.println("Making Object " + obj);
        return obj;
    }

    public void activateObject(Object obj) throws Exception {
        System.err.println("Activating Object " + obj);
    }

    public void passivateObject(Object obj) throws Exception {
        System.err.println("Passivating Object " + obj);
    }

    public boolean validateObject(Object obj) {
        /* 以1/2的概率將對象判定爲失效 */
        boolean result = (Math.random() > 0.5);
        System.err.println("Validating Object "
                + obj + " : " + result);
        return result;
    }

    public void destroyObject(Object obj) throws Exception {
        System.err.println("Destroying Object " + obj);
    }
}

使用ObjectPool
有了合適的PoolableObjectFactory之後,便可以開始請出ObjectPool來與之同臺演出了。

ObjectPool是在org.apache.commons.pool包中定義的一個接口,實際使用的時候也需要利用這個接口的一個具體實現。Pool組件本身包含了若干種現成的ObjectPool實現,可以直接利用。如果都不合用,也可以根據情況自行創建。具體的創建方法,可以參看Pool組件的文檔和源碼。

ObjectPool的使用方法類似這樣:

  1. 生成一個要用的PoolableObjectFactory類的實例。

    
    PoolableObjectFactory factory = new PoolableObjectFactorySample();
    
  2. 利用這個PoolableObjectFactory實例爲參數,生成一個實現了ObjectPool接口的類(例如StackObjectPool)的實例,作爲對象池。

    
    ObjectPool pool = new StackObjectPool(factory);
    
  3. 需要從對象池中取出對象時,調用該對象池的Object borrowObject()方法。

    
    Object obj = null;
    obj = pool.borrowObject();
    
  4. 需要將對象放回對象池中時,調用該對象池的void returnObject(Object obj)方法。

    
    pool.returnObject(obj);
    
  5. 當不再需要使用一個對象池時,調用該對象池的void close()方法,釋放它所佔據的資源。

    
    pool.close();
    

這些操作都可能會拋出異常,需要另外處理。

比較完整的使用ObjectPool的全過程,可以參考這段代碼:


ObjectPoolSample.java 

import org.apache.commons.pool.ObjectPool;
import org.apache.commons.pool.PoolableObjectFactory;
import org.apache.commons.pool.impl.StackObjectPool;

public class ObjectPoolSample {

    public static void main(String[] args) {
        Object obj = null;
        PoolableObjectFactory factory 
                = new PoolableObjectFactorySample();
        ObjectPool pool = new StackObjectPool(factory);
        try {
            for(long i = 0; i < 100 ; i++) {
                System.out.println("== " + i + " ==");
                obj = pool.borrowObject();
                System.out.println(obj);
                pool.returnObject(obj);
            }
            obj = null;//明確地設爲null,作爲對象已歸還的標誌
        }
        catch (Exception e) {
            e.printStackTrace();
        }
        finally {
            try{
                if (obj != null) {//避免將一個對象歸還兩次
                    pool.returnObject(obj);
                }
                pool.close();
            }
            catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

另外,ObjectPool接口還定義了幾個可以由具體的實現決定要不要支持的操作,包括:

void clear()

清除所有當前在此對象池中休眠的對象。

int getNumActive()

返回已經從此對象池中借出的對象的總數。

int getNumIdle()

返回當前在此對象池中休眠的對象的數目。

void setFactory(PoolableObjectFactory factory)

將當前對象池與參數中給定的PoolableObjectFactory相關聯。如果在當前狀態下,無法完成這一操作,會有一個IllegalStateException異常拋出。

如果所用的ObjectPool實現不支持這些操作,那麼調用這些方法的時候,會拋出一個UnsupportedOperationException異常。

利用ObjectPoolFactory
有時候,要在多處生成類型和設置都相同的ObjectPool。如果在每個地方都重寫一次調用相應構造方法的代碼,不但比較麻煩,而且日後修改起來,也有所不便。這種時候,正是使用ObjectPoolFactory的時機。

ObjectPoolFactory是一個在org.apache.commons.pool中定義的接口,它定義了一個稱爲ObjectPool createPool()方法,可以用於大量生產類型和設置都相同的ObjectPool。

Pool組件中,對每一個ObjectPool實現,都有一個對應的ObjectPoolFactory實現。它們相互之間,有一一對應的參數相同的構造方法。使用的時候,只要先用想要的參數和想用的ObjectPoolFactory實例,構造出一個ObjectPoolFactory對象,然後在需要生成ObjectPool的地方,調用這個對象的createPool()方法就可以了。日後無論想要調整所用ObjectPool的參數還是類型,只需要修改這一處,就可以大功告成了。

《使用ObjectPool》一節中的例子,改爲使用ObjectPoolFactory來生成所用的ObjectPool對象之後,基本就是這種形式:


ObjectPoolFactorySample.java

import org.apache.commons.pool.ObjectPool;
import org.apache.commons.pool.ObjectPoolFactory;
import org.apache.commons.pool.PoolableObjectFactory;
import org.apache.commons.pool.impl.StackObjectPoolFactory;

public class ObjectPoolFactorySample {

    public static void main(String[] args) {
        Object obj = null;
        PoolableObjectFactory factory
                = new PoolableObjectFactorySample();
        ObjectPoolFactory poolFactory
                = new StackObjectPoolFactory(factory);
        ObjectPool pool = poolFactory.createPool();
        try {
            for(long i = 0; i < 100 ; i++) {
                System.out.println("== " + i + " ==");
                obj = pool.borrowObject();
                System.out.println(obj);
                pool.returnObject(obj);
            }
            obj = null;
        }
        catch (Exception e) {
            e.printStackTrace();
        }
        finally {
            try{
                if (obj != null) {
                    pool.returnObject(obj);
                }
                pool.close();
            }
            catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

藉助BasePoolableObjectFactory
PoolableObjectFactory定義了許多方法,可以適應多種不同的情況。但是,在並沒有什麼特殊需要的時候,直接實現PoolableObjectFactory接口,就要編寫若干的不進行任何操作,或是始終返回true的方法來讓編譯通過,比較繁瑣。這種時候就可以藉助BasePoolableObjectFactory的威力,來簡化編碼的工作。

BasePoolableObjectFactory是org.apache.commons.pool包中的一個抽象類。它實現了PoolableObjectFactory接口,並且爲除了makeObject之外的方法提供了一個基本的實現——activateObject、passivateObject和destroyObject不進行任何操作,而validateObject始終返回true。通過繼承這個類,而不是直接實現PoolableObjectFactory接口,就可以免去編寫一些只起到讓編譯通過的作用的代碼的麻煩了。

這個例子展示了一個從BasePoolableObjectFactory擴展而來的PoolableObjectFactory:


BasePoolableObjectFactorySample.java 

import org.apache.commons.pool.BasePoolableObjectFactory;

public class BasePoolableObjectFactorySample 
        extends BasePoolableObjectFactory {

    private int counter = 0;

    public Object makeObject() throws Exception {
        return String.valueOf(counter++);
    }
}

各式各樣的ObjectPool
可口可樂公司的軟飲料有可口可樂、雪碧和芬達等品種,百事可樂公司的軟飲料有百事可樂、七喜和美年達等類型,而Pool組件提供的ObjectPool實現則有StackObjectPool、SoftReferenceObjectPool和GenericObjectPool等種類。

不同類型的軟飲料各有各自的特點,分別適應不同消費者的口味;而不同類型的ObjectPool也各有各自的特色,分別適應不同的情況。

StackObjectPool
StackObjectPool利用一個java.util.Stack對象來保存對象池裏的對象。這種對象池的特色是:

  • 可以爲對象池指定一個初始的參考大小(當空間不夠時會自動增長)。
  • 在對象池已空的時候,調用它的borrowObject方法,會自動返回新創建的實例。
  • 可以爲對象池指定一個可保存的對象數目的上限。達到這個上限之後,再向池裏送回的對象會被自動送去回收。

StackObjectPool的構造方法共有六個,其中:

  • 最簡單的一個是StackObjectPool(),一切採用默認的設置,也不指明要用的PoolableObjectFactory實例。
  • 最複雜的一個則是StackObjectPool(PoolableObjectFactory factory, int max, int init)。其中:
    • 參數factory指明要與之配合使用的PoolableObjectFactory實例;
    • 參數max設定可保存對象數目的上限;
    • 參數init則指明初始的參考大小。
  • 剩餘的四個構造方法則是最複雜的構造方法在某方面的簡化版本,可以根據需要選用。它們是:
    • StackObjectPool(int max)
    • StackObjectPool(int max, int init)
    • StackObjectPool(PoolableObjectFactory factory)
    • StackObjectPool(PoolableObjectFactory factory, int max)

用不帶factory參數的構造方法構造的StackObjectPool實例,必須要在用它的setFactory(PoolableObjectFactory factory)方法與某一PoolableObjectFactory實例關聯起來後才能正常使用。

這種對象池可以在沒有Jakarta Commmons Collections組件支持的情況下正常運行。

SoftReferenceObjectPool
SoftReferenceObjectPool利用一個java.util.ArrayList對象來保存對象池裏的對象。不過它並不在對象池裏直接保存對象本身,而是保存它們的“軟引用”(Soft Reference)。這種對象池的特色是:

  • 可以保存任意多個對象,不會有容量已滿的情況發生。
  • 在對象池已空的時候,調用它的borrowObject方法,會自動返回新創建的實例。
  • 可以在初始化同時,在池內預先創建一定量的對象。
  • 當內存不足的時候,池中的對象可以被Java虛擬機回收。

SoftReferenceObjectPool的構造方法共有三個,其中:

  • 最簡單的是SoftReferenceObjectPool(),不預先在池內創建對象,也不指明要用的PoolableObjectFactory實例。
  • 最複雜的一個則是SoftReferenceObjectPool(PoolableObjectFactory factory, int initSize)。其中:
    • 參數factory指明要與之配合使用的PoolableObjectFactory實例
    • 參數initSize則指明初始化時在池中創建多少個對象。
  • 剩下的一個構造方法,則是最複雜的構造方法在某方面的簡化版本,適合在大多數情況下使用。它是:
    • SoftReferenceObjectPool(PoolableObjectFactory factory)

用不帶factory參數的構造方法構造的SoftReferenceObjectPool實例,也要在用它的setFactory(PoolableObjectFactory factory)方法與某一PoolableObjectFactory實例關聯起來後才能正常使用。

這種對象池也可以在沒有Jakarta Commmons Collections組件支持的情況下正常運行。

GenericObjectPool
GenericObjectPool利用一個org.apache.commons.collections.CursorableLinkedList對象來保存對象池裏的對象。這種對象池的特色是:

  • 可以設定最多能從池中借出多少個對象。
  • 可以設定池中最多能保存多少個對象。
  • 可以設定在池中已無對象可借的情況下,調用它的borrowObject方法時的行爲,是等待、創建新的實例還是拋出異常。
  • 可以分別設定對象借出和還回時,是否進行有效性檢查。
  • 可以設定是否使用一個單獨的線程,對池內對象進行後臺清理。

GenericObjectPool的構造方法共有七個,其中:

  • 最簡單的一個是GenericObjectPool(PoolableObjectFactory factory)。僅僅指明要用的PoolableObjectFactory實例,其它參數則採用默認值。
  • 最複雜的一個是GenericObjectPool(PoolableObjectFactory factory, int maxActive, byte whenExhaustedAction, long maxWait, int maxIdle, boolean testOnBorrow, boolean testOnReturn, long timeBetweenEvictionRunsMillis, int numTestsPerEvictionRun, long minEvictableIdleTimeMillis, boolean testWhileIdle)。其中:
    • 參數factory指明要與之配合使用的PoolableObjectFactory實例。
    • 參數maxActive指明能從池中借出的對象的最大數目。如果這個值不是正數,表示沒有限制。
    • 參數whenExhaustedAction指定在池中借出對象的數目已達極限的情況下,調用它的borrowObject方法時的行爲。可以選用的值有:
      • GenericObjectPool.WHEN_EXHAUSTED_BLOCK,表示等待;
      • GenericObjectPool.WHEN_EXHAUSTED_GROW,表示創建新的實例(不過這就使maxActive參數失去了意義);
      • GenericObjectPool.WHEN_EXHAUSTED_FAIL,表示拋出一個java.util.NoSuchElementException異常。
    • 參數maxWait指明若在對象池空時調用borrowObject方法的行爲被設定成等待,最多等待多少毫秒。如果等待時間超過了這個數值,則會拋出一個java.util.NoSuchElementException異常。如果這個值不是正數,表示無限期等待。
    • 參數testOnBorrow設定在借出對象時是否進行有效性檢查。
    • 參數testOnBorrow設定在還回對象時是否進行有效性檢查。
    • 參數timeBetweenEvictionRunsMillis,設定間隔每過多少毫秒進行一次後臺對象清理的行動。如果這個值不是正數,則實際上不會進行後臺對象清理。
    • 參數numTestsPerEvictionRun,設定在進行後臺對象清理時,每次檢查幾個對象。如果這個值不是正數,則每次檢查的對象數是檢查時池內對象的總數乘以這個值的負倒數再向上取整的結果——也就是說,如果這個值是-2(-3、-4、-5……)的話,那麼每次大約檢查當時池內對象總數的1/2(1/3、1/4、1/5……)左右。
    • 參數minEvictableIdleTimeMillis,設定在進行後臺對象清理時,視休眠時間超過了多少毫秒的對象爲過期。過期的對象將被回收。如果這個值不是正數,那麼對休眠時間沒有特別的約束。
    • 參數testWhileIdle,則設定在進行後臺對象清理時,是否還對沒有過期的池內對象進行有效性檢查。不能通過有效性檢查的對象也將被回收。
    • 另一個比較特別的構造方法是GenericObjectPool(PoolableObjectFactory factory, GenericObjectPool.Config config) 。其中:
      • 參數factory指明要與之配合使用的PoolableObjectFactory實例;
      • 參數config則指明一個包括了各個參數的預設值的對象(詳見《GenericObjectPool.Config》一節)。
    • 剩下的五個構造函數則是最複雜的構造方法在某方面的簡化版本,可以根據情況選用。它們是:
      • GenericObjectPool(PoolableObjectFactory factory, int maxActive)
      • GenericObjectPool(PoolableObjectFactory factory, int maxActive, byte whenExhaustedAction, long maxWait)
      • GenericObjectPool(PoolableObjectFactory factory, int maxActive, byte whenExhaustedAction, long maxWait, boolean testOnBorrow, boolean testOnReturn)
      • GenericObjectPool(PoolableObjectFactory factory, int maxActive, byte whenExhaustedAction, long maxWait, int maxIdle)
      • GenericObjectPool(PoolableObjectFactory factory, int maxActive, byte whenExhaustedAction, long maxWait, int maxIdle, boolean testOnBorrow, boolean testOnReturn)

這種對象池不可以在沒有Jakarta Commmons Collections組件支持的情況下運行。

GenericObjectPool.Config
調用一個有很多的參數的方法的時候,很可能將參數的位置和個數搞錯,導致編譯或運行時的錯誤;閱讀包含了有很多參數的方法調用的代碼的時候,也很可能因爲沒有搞對參數的位置和個數,產生錯誤的理解。因此,人們往往避免給一個方法安排太多的參數的做法(所謂的“Long Parameter List”)。不過,有些方法又確實需要許多參數才能完成工作。於是,就有人想到了一種將大批的參數封裝到一個對象(稱爲參數對象,Parameter Object)裏,然後將這個對象作爲單一的參數傳遞的兩全其美的對策。

因爲生成GenericKeyedObjectPool時可供設置的特性非常之多,所以它的構造方法裏也就難免會需要不少的參數。GenericKeyedObjectPool除了提供了幾個超長的構造方法之外,同時也定義了一個使用參數對象的構造方法。所用參數對象的類型是GenericKeyedObjectPool.Config。

GenericKeyedObjectPool.Config定義了許多的public字段,每個對應一種可以爲GenericKeyedObjectPool設置的特性,包括:

  • int maxActive
  • int maxIdle
  • long maxWait
  • long minEvictableIdleTimeMillis
  • int numTestsPerEvictionRun
  • boolean testOnBorrow
  • boolean testOnReturn
  • boolean testWhileIdle
  • long timeBetweenEvictionRunsMillis
  • byte whenExhaustedAction

這些字段的作用,與在GenericKeyedObjectPool最複雜的構造方法中與它們同名的參數完全相同。

使用的時候,先生成一個GenericKeyedObjectPool.Config對象,然後將個字段設置爲想要的值,最後用這個對象作爲唯一的參數調用GenericKeyedObjectPool的構造方法即可。

注意:使用有許多public字段、卻沒有任何方法的類,也是一個人們往往加以避免的行爲(所謂的“Data Class”)。不過這次GenericKeyedObjectPool特立獨行了一回。

帶鍵值的對象池
有時候,單用對池內所有對象一視同仁的對象池,並不能解決的問題。例如,對於一組某些參數設置不同的同類對象——比如一堆指向不同地址的java.net.URL對象或者一批代表不同語句的java.sql.PreparedStatement對象,用這樣的方法池化,就有可能取出不合用的對象的麻煩。

可以通過爲每一組參數相同的同類對象建立一個單獨的對象池來解決這個問題。但是,如果使用普通的ObjectPool來實施這個計策的話,因爲普通的PoolableObjectFactory只能生產出大批設置完全一致的對象,就需要爲每一組參數相同的對象編寫一個單獨的PoolableObjectFactory,工作量相當可觀。這種時候就適合調遣Pool組件中提供的一種“帶鍵值的對象池”來展開工作了。

Pool組件採用實現了KeyedObjectPool接口的類,來充當帶鍵值的對象池。相應的,這種對象池需要配合實現了KeyedPoolableObjectFactory接口的類和實現了KeyedObjectPoolFactory接口的類來使用(這三個接口都在org.apache.commons.pool包中定義):

  • KeyedPoolableObjectFactory和PoolableObjectFactory形式如出一轍,只是每個方法都增加了一個Object key參數而已:
    • makeObject的參數變爲(Object key)
    • activateObject的參數變爲(Object key, Object obj)
    • passivateObject的參數變爲(Object key, Object obj)
    • validateObject的參數變爲Object key, Object obj)
    • destroyObject的參數變爲(Object key, Object obj)

    另外Pool組件也提供了BaseKeyedPoolableObjectFactory,用於充當和BasePoolableObjectFactory差不多的角色。

  • KeyedObjectPool和ObjectPool的形式大同小異,只是某些方法的參數類型發生了變化,某些方法分成了兩種略有不同的版本:
    • 用Object borrowObject(Object key)和void returnObject(Object key, Object obj)來負責對象出借和歸還的動作。
    • 用void close()來關閉不再需要的對象池。
    • 用void clear(Object key)和void clear()來清空池中的對象,前者針對與特定鍵值相關聯的實例,後者針對整個對象池。
    • 用int getNumActive(Object key)和int getNumActive()來查詢已借出的對象數,前者針對與特定鍵值相關聯的實例,後者針對整個對象池。
    • 用int getNumIdle(Object key)和int getNumIdle()來查詢正在休眠的對象數,前者針對與特定鍵值相關聯的實例,後者針對整個對象池。
    • 用void setFactory(KeyedPoolableObjectFactory factory)來設置要用的KeyedPoolableObjectFactory實例。

    void clear、int getNumActive、int getNumIdle和void setFactory的各種版本都仍然是可以由具體實現自行決定是否要支持的方法。如果所用的KeyedObjectPool實現不支持這些操作,那麼調用這些方法的時候,會拋出一個UnsupportedOperationException異常。

  • KeyedObjectPoolFactory和ObjectPoolFactory的形式完全相同,只是所代表的對象不同而已。

這一類對象池的基本使用方法接近於這樣:


KeyedObjectPoolSample.java 

import org.apache.commons.pool.BaseKeyedPoolableObjectFactory;
import org.apache.commons.pool.KeyedObjectPool;
import org.apache.commons.pool.KeyedObjectPoolFactory;
import org.apache.commons.pool.KeyedPoolableObjectFactory;
import org.apache.commons.pool.impl.StackKeyedObjectPoolFactory;

class KeyedPoolableObjectFactorySample
        extends BaseKeyedPoolableObjectFactory {

    public Object makeObject(Object key) throws Exception {
        return new String("[" + key.hashCode() + "]");
    }

}

public class KeyedObjectPoolSample {
    public static void main(String[] args) {
        Object obj = null;
        KeyedPoolableObjectFactory factory
                = new KeyedPoolableObjectFactorySample();
        KeyedObjectPoolFactory poolFactory 
                = new StackKeyedObjectPoolFactory(factory);
        KeyedObjectPool pool = poolFactory.createPool();
        String key = null;
        try {
            for (long i = 0; i < 100 ; i++) {
                 key = "" + (int) (Math.random() * 10);
                System.out.println("== " + i + " ==");
                System.out.println("Key:" + key);
                obj = pool.borrowObject(key);
                System.out.println("Object:" + obj);
                pool.returnObject(key, obj);
                obj = null;
            }
        }
        catch (Exception e) {
                 e.printStackTrace();
        }
        finally {
            try{
                if (obj != null) {
                    pool.returnObject(key, obj);
                }
                pool.close();
            }
            catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

Pool組件自帶的KeyedObjectPool的實現有StackKeyedObjectPool和GenericKeyedObjectPool兩種。它們的使用方法分別與它們各自的近親KeyedObjectPool和KeyedObjectPool基本一致,只是原來使用GenericObjectPool.Config的地方要使用GenericKeyedObjectPool.Config代替。

當出借少於歸還
Java並未提供一種機制來保證兩個方法被調用的次數之間呈現一種特定的關係(相等,相差一個常數,或是其它任何關係)。因此,完全可以做到建立一個ObjectPool對象,然後調用一次borrowObject方法,借出一個對象,之後重複兩次returnObject方法調用,進行兩次歸還。而調用一個從不曾借出對象的ObjectPool的returnObject方法也並不是一個不可完成的任務。

儘管這些使用方法並不合乎returnObject的字面意思,但是Pool組件中的各個ObjectPool/KeyedObjectPool實現都不在乎這一點。它們的returnObject方法都只是單純地召喚與當前對象池關聯的PoolableObjectFactory實例,看這對象能否經受得起validateObject的考驗而已。考驗的結果決定了這個對象是應該拿去作passivateObject處理,而後留待重用;還是應該拿去作destroyObject處理,以免佔用資源。也就是說,當出借少於歸還的時候,並不會額外發生什麼特別的事情(當然,有可能因爲該對象池處於不接受歸還對象的請求的狀態而拋出異常,不過這是常規現象)。

在實際使用中,可以利用這一特性來向對象池內加入通過其它方法生成的對象。

線程安全問題
有時候可能要在多線程環境下使用Pool組件,這時候就會遇到和Pool組件的線程安全程度有關的問題。

因爲ObjectPool和KeyedObjectPool都是在org.apache.commons.pool中定義的接口,而在接口中無法使用“synchronized”來修飾方法,所以,一個ObjectPool/KeyedObjectPool下的各個方法是否是同步方法,完全要看具體的實現。而且,單純地使用了同步方法,也並不能使對象就此在多線程環境裏高枕無憂。

就Pool組件中自帶的幾個ObjectPool/KeyedObjectPool的實現而言,它們都在一定程度上考慮了在多線程環境中使用的情況。不過還不能說它們是完全“線程安全”的。

例如,這段代碼有些時候就會有一些奇怪的表現,最後輸出的結果比預期的要大:


UnsafeMultiThreadPoolingSample.java

import org.apache.commons.pool.ObjectPool;
import org.apache.commons.pool.impl.StackObjectPool;

class UnsafePicker extends Thread {
    private ObjectPool pool;
    public UnsafePicker(ObjectPool op) {
        pool = op;
    }
    public void run() {
        Object obj = null;
        try {
        /* 似乎…… */
            if ( pool.getNumActive() < 5 ) {
                sleep((long) (Math.random() * 10));
                obj = pool.borrowObject();
            }
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
}

public class UnsafeMultiThreadPoolingSample {

    public static void main(String[] args) {
        ObjectPool pool = new StackObjectPool
                (new BasePoolableObjectFactorySample());
        Thread ts[] = new Thread[20];
        for (int j = 0; j < ts.length; j++) {
            ts[j] =  new UnsafePicker(pool);
            ts[j].start();
        }
        try {
            Thread.sleep(1000);
            /* 然而…… */
            System.out.println("NumActive:" + pool.getNumActive());
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
}


要避免這種情況,就要進一步採取一些措施才行:


SafeMultiThreadPoolingSample.java 

import org.apache.commons.pool.ObjectPool;
import org.apache.commons.pool.impl.StackObjectPool;

class SafePicker extends Thread {
    private ObjectPool pool;
    public SafePicker(ObjectPool op) {
        pool = op;
    }
    public void run() {
        Object obj = null;
        try {
            /* 略加處理 */
            synchronized (pool) {
                if ( pool.getNumActive() < 5 ) {
                    sleep((long) (Math.random() * 10));
                    obj = pool.borrowObject();
                }
            }
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
}

public class SafeMultiThreadPoolingSample {

    public static void main(String[] args) {
        ObjectPool pool = new StackObjectPool
                (new BasePoolableObjectFactorySample());
        Thread ts[] = new Thread[20];
        for (int j = 0; j < ts.length; j++) {
            ts[j] =  new SafePicker(pool);
            ts[j].start();
        }
        try {
            Thread.sleep(1000);
            System.out.println("NumActive:" + pool.getNumActive());
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
}

基本上,可以說Pool組件是線程相容的。但是要在多線程環境中使用,還需要作一些特別的處理。

什麼時候不要池化
採用對象池化的本意,是要通過減少對象生成的次數,減少花在對象初始化上面的開銷,從而提高整體的性能。然而池化處理本身也要付出代價,因此,並非任何情況下都適合採用對象池化。

Dr. Cliff Click在JavaOne 2003上發表的《Performance Myths Exposed》中,給出了一組其它條件都相同時,使用與不使用對象池化技術的實際性能的比較結果。他的實測結果表明:

  • 對於類似Point這樣的輕量級對象,進行池化處理後,性能反而下降,因此不宜池化;
  • 對於類似Hashtable這樣的中量級對象,進行池化處理後,性能基本不變,一般不必池化(池化會使代碼變複雜,增大維護的難度);
  • 對於類似JPanel這樣的重量級對象,進行池化處理後,性能有所上升,可以考慮池化。

根據使用方法的不同,實際的情況可能與這一測量結果略有出入。在配置較高的機器和技術較強的虛擬機上,不宜池化的對象的範圍可能會更大。不過,對於像網絡和數據庫連接這類重量級的對象來說,目前還是有池化的必要。

基本上,只在重複生成某種對象的操作成爲影響性能的關鍵因素的時候,才適合進行對象池化。如果進行池化所能帶來的性能提高並不重要的話,還是不採用對象池化技術,以保持代碼的簡明,而使用更好的硬件和更棒的虛擬機來提高性能爲佳。

結束語
恰當地使用對象池化,可以有效地降低頻繁生成某些對象所造成的開銷,從而提高整體的性能。而藉助Jakarta Commons Pool組件,可以有效地減少花在處理對象池化上的工作量,進而,向其它重要的工作裏,投入更多的時間和精力。

參考資料

  • 很多疑難問題的答案都可以通過查閱Pool組件的Javadoc文檔和源代碼的方法解決。

  • Pool組件的官方站點 上,還可以進一步得到許多有用的信息。

  • DBCP 是一個基於Pool組件的Java數據庫連接池管理組件,同時也可以作爲Pool組件的用法示例使用。

  • 蔡學鏞在 《Java夜未眠(Sleepless Java)》 中的 《測不準原理》一文裏,介紹了Java中的包括“軟引用”(Soft Reference)在內的各種不同的引用的特點和用處。

  • Martin Fowler在 《Refactoring -- Improving the Design of Existing Code》(中譯本名爲《重構——改善既有代碼的設計》,由侯捷、熊節合譯)一書的第三章《代碼的壞味道(Bad Smells in Code)》中討論了被稱爲“Long Parameter List”和“Data Class”的“壞味道”。並指明瞭一些可以用於對付這些問題的重構手法。

  • Brian Goetz在IBM developerWorks上發表的《Java 理論與實踐:描繪線程安全性》一文中,說明了爲什麼單純地使用同步方法還不能讓對象就此在多線程環境裏高枕無憂的原因。

  • Dr. Cliff Click發表在JavaOne 2003上的《Performance Myths Exposed》(Session #1522),給出了一組包括“對象池化”在內的、對“能提高Java程序性能”的做法的實際效果的測試數據,和一些恰當使用這些做法的建議。
發佈了0 篇原創文章 · 獲贊 0 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章