简介: 恰当地使用对象池化技术,可以有效地减少对象生成和初始化时的消耗,提高系统的运行效率。Jakarta Commons Pool组件提供了一整套用于实现对象池化的框架,以及若干种各具特色的对象池实现,可以有效地减少处理对象池化时的工作量,为其它重要的工作留下更多的精力和时间。
<!-- <p class="ibm-no-print"> <div id="dw-tag-this" class="ibm-no-print"></div> <div id="interestShow" class="ibm-no-print"></div> </p> -->
发布日期: 2003 年 12 月 12 日
级别: 初级
访问情况 2644 次浏览
建议: 0 (添加评论) <!-- Rating_Area_Begin --><!-- Ensure that div id is based on input id and ends with -widget -->
创建新的对象并初始化的操作,可能会消耗很多的时间。在这种对象的初始化工作包含了一些费时的操作(例如,从一台位于20,000千米以外的主机上读出一些数据)的时候,尤其是这样。在需要大量生成这样的对象的时候,就可能会对性能造成一些不可忽略的影响。要缓解这个问题,除了选用更好的硬件和更棒的虚拟机以外,适当地采用一些能够减少对象创建次数的编码技巧,也是一种有效的对策。对象池化技术(Object Pooling)就是这方面的著名技巧,而Jakarta Commons Pool组件则是处理对象池化的得力外援。
对象池化的基本思路是:将用过的对象保存起来,等下一次需要这种对象的时候,再拿出来重复使用,从而在一定程度上减少频繁创建对象所造成的开销。用于充当保存对象的“容器”的对象,被称为“对象池”(Object Pool,或简称Pool)。
对于没有状态的对象(例如String),在重复使用之前,无需进行任何处理;对于有状态的对象(例如StringBuffer),在重复使用之前,就需要把它们恢复到等同于刚刚生成时的状态。由于条件的限制,恢复某个对象的状态的操作不可能实现了的话,就得把这个对象抛弃,改用新创建的实例了。
并非所有对象都适合拿来池化――因为维护对象池也要造成一定开销。对生成时开销不大的对象进行池化,反而可能会出现“维护对象池的开销”大于“生成新对象的开销”,从而使性能降低的情况。但是对于生成时开销可观的对象,池化技术就是提高性能的有效策略了。
Jakarta Commons Pool是一个用于在Java程序中实现对象池化的组件。它的基本情况是:
为了顺利的按照本文中提到的方法使用Pool组件,除去Java 2 SDK外,还需要先准备下列一些东西:
以上两种软件均有已编译包和源代码包两种形式可供选择。一般情况下,使用已编译包即可。不过建议同时也下载源代码包,作为参考资料使用。
如果打算使用源代码包自行编译,那么还需要准备以下一些东西:
具体的编译方法,可以参看有关的Ant文档。
将解压或编译后得到的commons-pool.jar和commons-collections.jar放入CLASSPATH,就可以开始使用Pool组件了。
PoolableObjectFactory、ObjectPool和ObjectPoolFactory
在Pool组件中,对象池化的工作被划分给了三类对象:
相应地,使用Pool组件的过程,也大体可以划分成“创立PoolableObjectFactory”、“使用ObjectPool”和可选的“利用ObjectPoolFactory”三种动作。
Pool组件利用PoolableObjectFactory来照看被池化的对象。ObjectPool的实例在需要处理被池化的对象的产生、激活、挂起、校验和销毁工作时,就会调用跟它关联在一起的PoolableObjectFactory实例的相应方法来操作。
PoolableObjectFactory是在org.apache.commons.pool包中定义的一个接口。实际使用的时候需要利用这个接口的一个具体实现。Pool组件本身没有包含任何一种PoolableObjectFactory实现,需要根据情况自行创立。
创立PoolableObjectFactory的大体步骤是:
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) { 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); } |
最后完成的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); } } |
有了合适的PoolableObjectFactory之后,便可以开始请出ObjectPool来与之同台演出了。
ObjectPool是在org.apache.commons.pool包中定义的一个接口,实际使用的时候也需要利用这个接口的一个具体实现。Pool组件本身包含了若干种现成的ObjectPool实现,可以直接利用。如果都不合用,也可以根据情况自行创建。具体的创建方法,可以参看Pool组件的文档和源码。
ObjectPool的使用方法类似这样:
PoolableObjectFactory factory = new PoolableObjectFactorySample(); |
ObjectPool pool = new StackObjectPool(factory); |
Object obj = null; obj = pool.borrowObject(); |
pool.returnObject(obj); |
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。如果在每个地方都重写一次调用相应构造方法的代码,不但比较麻烦,而且日后修改起来,也有所不便。这种时候,正是使用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(); } } } } |
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++); } } |
可口可乐公司的软饮料有可口可乐、雪碧和芬达等品种,百事可乐公司的软饮料有百事可乐、七喜和美年达等类型,而Pool组件提供的ObjectPool实现则有StackObjectPool、SoftReferenceObjectPool和GenericObjectPool等种类。
不同类型的软饮料各有各自的特点,分别适应不同消费者的口味;而不同类型的ObjectPool也各有各自的特色,分别适应不同的情况。
StackObjectPool利用一个java.util.Stack对象来保存对象池里的对象。这种对象池的特色是:
StackObjectPool的构造方法共有六个,其中:
用不带factory参数的构造方法构造的StackObjectPool实例,必须要在用它的setFactory(PoolableObjectFactory factory)方法与某一PoolableObjectFactory实例关联起来后才能正常使用。
这种对象池可以在没有Jakarta Commmons Collections组件支持的情况下正常运行。
SoftReferenceObjectPool利用一个java.util.ArrayList对象来保存对象池里的对象。不过它并不在对象池里直接保存对象本身,而是保存它们的“软引用”(Soft Reference)。这种对象池的特色是:
SoftReferenceObjectPool的构造方法共有三个,其中:
用不带factory参数的构造方法构造的SoftReferenceObjectPool实例,也要在用它的setFactory(PoolableObjectFactory factory)方法与某一PoolableObjectFactory实例关联起来后才能正常使用。
这种对象池也可以在没有Jakarta Commmons Collections组件支持的情况下正常运行。
GenericObjectPool利用一个org.apache.commons.collections.CursorableLinkedList对象来保存对象池里的对象。这种对象池的特色是:
GenericObjectPool的构造方法共有七个,其中:
这种对象池不可以在没有Jakarta Commmons Collections组件支持的情况下运行。
调用一个有很多的参数的方法的时候,很可能将参数的位置和个数搞错,导致编译或运行时的错误;阅读包含了有很多参数的方法调用的代码的时候,也很可能因为没有搞对参数的位置和个数,产生错误的理解。因此,人们往往避免给一个方法安排太多的参数的做法(所谓的“Long Parameter List”)。不过,有些方法又确实需要许多参数才能完成工作。于是,就有人想到了一种将大批的参数封装到一个对象(称为参数对象,Parameter Object)里,然后将这个对象作为单一的参数传递的两全其美的对策。
因为生成GenericKeyedObjectPool时可供设置的特性非常之多,所以它的构造方法里也就难免会需要不少的参数。GenericKeyedObjectPool除了提供了几个超长的构造方法之外,同时也定义了一个使用参数对象的构造方法。所用参数对象的类型是GenericKeyedObjectPool.Config。
GenericKeyedObjectPool.Config定义了许多的public字段,每个对应一种可以为GenericKeyedObjectPool设置的特性,包括:
这些字段的作用,与在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包中定义):
另外Pool组件也提供了BaseKeyedPoolableObjectFactory,用于充当和BasePoolableObjectFactory差不多的角色。
void clear、int getNumActive、int getNumIdle和void setFactory的各种版本都仍然是可以由具体实现自行决定是否要支持的方法。如果所用的KeyedObjectPool实现不支持这些操作,那么调用这些方法的时候,会抛出一个UnsupportedOperationException异常。
这一类对象池的基本使用方法接近于这样:
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》中,给出了一组其它条件都相同时,使用与不使用对象池化技术的实际性能的比较结果。他的实测结果表明:
根据使用方法的不同,实际的情况可能与这一测量结果略有出入。在配置较高的机器和技术较强的虚拟机上,不宜池化的对象的范围可能会更大。不过,对于像网络和数据库连接这类重量级的对象来说,目前还是有池化的必要。
基本上,只在重复生成某种对象的操作成为影响性能的关键因素的时候,才适合进行对象池化。如果进行池化所能带来的性能提高并不重要的话,还是不采用对象池化技术,以保持代码的简明,而使用更好的硬件和更棒的虚拟机来提高性能为佳。
恰当地使用对象池化,可以有效地降低频繁生成某些对象所造成的开销,从而提高整体的性能。而借助Jakarta Commons Pool组件,可以有效地减少花在处理对象池化上的工作量,进而,向其它重要的工作里,投入更多的时间和精力。
<!-- CMA ID: 53399 --><!-- Site ID: 10 --><!-- XSLT stylesheet used to transform this file: dw-article-6.0-beta.xsl -->孙海涛从1994年6月的一个风雨交加的下午开始了他的编程生涯。目前,他的兴趣集中于Java、Web、开源软件和人机交互。但是,这并不表示他不会对其它的事物给予足够的关心和重视。可以通过 [email protected]与他取得联系。