原文链接:Creating Object Pool in Java
在这篇文章中,我们将对于如何用java写一个对象池一探究竟。
进来,JVM的性能已经在各个方面都有了较大的提升,因此常见的对象创建已不再像从前那样代价高昂了。然而,仍然有一些“称不上是轻量级的”对象,他们的创建仍然较为昂贵,比如:数据库连接对象、解析器对象、线程对象等等。在应用中我们常常需要创建大量的此类对象,然而因为它们的实例化耗时甚多,这对应用的性能影响极大。对此,我们试图利用多次复用同一个对象来解决。
对象池于是也应运而生。形象地说,对象池可以被想象为一个对象的存储空间,其存在允许我们在其中存储对象,并且动态地使用和复用它们。对象池还要操控其中对象的生命周期。
需求已定,talk is cheap,show me the code!幸运的是,已经有许多开源的对象池项目存在于世,我们也就不必重新造轮子了。
在这篇文章中我们将基于apach commons pool来写我们自己的对象池。我们用的是2.2版本,因为这是在这篇文章写就时的最新版本。
基础内容:
1. 一个用于存储重量级对象(池中对象)的对象池。
1. 一个简单的借口使得客户端可以:
* 暂借池中对象为己用
* 用完后归还暂借的对象
让我们从解析器对象着手。
解析器对象的设计目的是为了解析诸如xml文件、html文件之类的文档。为每一个xml文件(都有类似的结构)创建一个新的xml解析器异常昂贵。我们都会想要复用同一个(或者在并发环境中的多个)解析器对象来解析xml。在这种情况下,我们可以将一些解析器对象扔进对象池里面,使它们可以在有需求时被复用。
先来看一个非常简单的解析器接口:
package blog.techcypher.parser;
/**
* Abstract definition of Parser.
*
* @author abhishek
*
*/
public interface Parser<E, T> {
/**
* Parse the element E and set the result back into target object T.
*
* @param elementToBeParsed
* @param result
* @throws Exception
*/
public void parse(E elementToBeParsed, T result) throws Exception;
/**
* Tells whether this parser is valid or not. This will ensure the we
* will never be using an invalid/corrupt parser.
*
* @return
*/
public boolean isValid();
/**
* Reset parser state back to the original, so that it will be as
* good as new parser.
*
*/
public void reset();
}
再如下这样实现一个xml解析器:
package blog.techcypher.parser.impl;
import blog.techcypher.parser.Parser;
/**
* Parser for parsing xml documents.
*
* @author abhishek
*
* @param <E>
* @param <T>
*/
public class XmlParser<E, T> implements Parser<E, T> {
private Exception exception;
@Override
public void parse(E elementToBeParsed, T result) throws Exception {
try {
System.out.println("[" + Thread.currentThread().getName()+ "]: Parser Instance:" + this);
// Do some real parsing stuff.
} catch(Exception e) {
this.exception = e;
e.printStackTrace(System.err);
throw e;
}
}
@Override
public boolean isValid() {
return this.exception == null;
}
@Override
public void reset() {
this.exception = null;
}
}
我们已经有了解析器对象,现在是时候写个对象池了。
这里,我们将使用GenericObjectPool来存储解析器对象。Apache commons pool中包含对象池的实现。GenericObjectPool可被用于存储任何对象。每个池对应一个对象工厂并且包含相同类型的对象。
GenericObjectPool提供了大量的设置选项,包括限制空闲/活动实例的数量,释放掉池中的空闲对象等等。
如果想要为不同类型的对象(比如解析器、转换器、设备连接等等)创建池的话,就应该用GenericKeyedObjectPool。
package blog.techcypher.parser.pool;
import org.apache.commons.pool2.PooledObjectFactory;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import blog.techcypher.parser.Parser;
/**
* Pool Implementation for Parser Objects.
* It is an implementation of ObjectPool.
*
* It can be visualized as-
* +-------------------------------------------------------------+
* | ParserPool |
* +-------------------------------------------------------------+
* | [Parser@1, Parser@2,...., Parser@N] |
* +-------------------------------------------------------------+
*
* @author abhishek
*
* @param <E>
* @param <T>
*/
public class ParserPool<E, T> extends GenericObjectPool<Parser<E, T>>{
/**
* Constructor.
*
* It uses the default configuration for pool provided by
* apache-commons-pool2.
*
* @param factory
*/
public ParserPool(PooledObjectFactory<Parser<E, T>> factory) {
super(factory);
}
/**
* Constructor.
*
* This can be used to have full control over the pool using configuration
* object.
*
* @param factory
* @param config
*/
public ParserPool(PooledObjectFactory<Parser<E, T>> factory,
GenericObjectPoolConfig config) {
super(factory, config);
}
}
显而易见,对象池的构造函数需要一个对象工厂作为参数来管理池中对象的生命周期。因此我们需要一个能够生产解析器对象的“解析器对象工厂”。
Commons pool提供了对象工厂(PooledObjectFactory)的泛型接口。PooledObjectFactory生产并且管理池中对象(PooledObjects)。这些对象的包装管理池的状态,允许PooledObjectFactory的方法能够接触到诸如实例创建时间或者上次使用时间之类的数据。
DefaultPooledObject提供了一个池状态管理方法集的自然实现。实现PoolableObjectFactory的最简方法就是继承BasePooledObjectFactory。这个工厂提供一个makeObject()
方法并且返回wrap(create())
,而wrap()
和create()
都是抽象的。我们实现create()
来生产我们需要在对象池中管理的对象以及wrap()
来包装PooledObjects中的对象实例。
我们对于解析器对象的工厂实现如下:
package blog.techcypher.parser.pool;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import blog.techcypher.parser.Parser;
import blog.techcypher.parser.impl.XmlParser;
/**
* Factory to create parser object(s).
*
* @author abhishek
*
* @param <E>
* @param <T>
*/
public class ParserFactory<E, T> extends BasePooledObjectFactory<Parser<E, T>> {
@Override
public Parser<E, T> create() throws Exception {
return new XmlParser<E, T>();
}
@Override
public PooledObject<Parser<E, T>> wrap(Parser<E, T> parser) {
return new DefaultPooledObject<Parser<E,T>>(parser);
}
@Override
public void passivateObject(PooledObject<Parser<E, T>> parser) throws Exception {
parser.getObject().reset();
}
@Override
public boolean validateObject(PooledObject<Parser<E, T>> parser) {
return parser.getObject().isValid();
}
}
现在我们已经成功地建立了我们的对象池来储存解析器对象,还有一个能够管理解析器对象的工厂。
请注意,我们实现了一些多余的方法:
1. boolean validateObject(PooledObject obj):它被用来基于配置验证从池中借走或还回的对象是否可用。默认地,这种验证是关闭的。实现这个方法的目的是保证用户能够永远从池中拿到可用的对象。
2. void passivateObject(PooledObject obj):它在对象还回对象池时被调用。实现中,我们可以在其中重置对象状态,来使对象能够下次被借走时工作地完好如初。
万事俱备,只欠东风,现在我们来为这个对象池写一个测试。对象池的用户可以:
1. 通过调用pool.borrowObject()
来获得对象
2. 通过调用pool.returnObject(Object)
来归还对象
“解析器对象池的”测试代码如下所示:
package blog.techcypher.parser;
import static org.junit.Assert.fail;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import junit.framework.Assert;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.junit.Before;
import org.junit.Test;
import blog.techcypher.parser.pool.ParserFactory;
import blog.techcypher.parser.pool.ParserPool;
/**
* Test case to test-
* 1. object creation by factory
* 2. object borrow from pool.
* 3. returning object back to pool.
*
* @author abhishek
*
*/
public class ParserFactoryTest {
private ParserPool<String, String> pool;
private AtomicInteger count = new AtomicInteger(0);
@Before
public void setUp() throws Exception {
GenericObjectPoolConfig config = new GenericObjectPoolConfig();
config.setMaxIdle(1);
config.setMaxTotal(1);
/*---------------------------------------------------------------------+
|TestOnBorrow=true --> To ensure that we get a valid object from pool |
|TestOnReturn=true --> To ensure that valid object is returned to pool |
+---------------------------------------------------------------------*/
config.setTestOnBorrow(true);
config.setTestOnReturn(true);
pool = new ParserPool<String, String>(new ParserFactory<String, String>(), config);
}
@Test
public void test() {
try {
int limit = 10;
ExecutorService es = new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(limit));
for (int i=0; i<limit; i++) {
Runnable r = new Runnable() {
@Override
public void run() {
Parser<String, String> parser = null;
try {
parser = pool.borrowObject();
count.getAndIncrement();
parser.parse(null, null);
} catch (Exception e) {
e.printStackTrace(System.err);
} finally {
if (parser != null) {
pool.returnObject(parser);
}
}
}
};
es.submit(r);
}
es.shutdown();
try {
es.awaitTermination(1, TimeUnit.MINUTES);
} catch (InterruptedException ignored) {}
System.out.println("Pool Stats:\n Created:[" + pool.getCreatedCount() + "], Borrowed:[" + pool.getBorrowedCount() + "]");
Assert.assertEquals(limit, count.get());
Assert.assertEquals(count.get(), pool.getBorrowedCount());
Assert.assertEquals(1, pool.getCreatedCount());
} catch (Exception ex) {
fail("Exception:" + ex);
}
}
}
运行结果:
[pool-1-thread-1]: Parser Instance:blog.techcypher.parser.impl.XmlParser@fcfa52
[pool-1-thread-2]: Parser Instance:blog.techcypher.parser.impl.XmlParser@fcfa52
[pool-1-thread-3]: Parser Instance:blog.techcypher.parser.impl.XmlParser@fcfa52
[pool-1-thread-4]: Parser Instance:blog.techcypher.parser.impl.XmlParser@fcfa52
[pool-1-thread-5]: Parser Instance:blog.techcypher.parser.impl.XmlParser@fcfa52
[pool-1-thread-8]: Parser Instance:blog.techcypher.parser.impl.XmlParser@fcfa52
[pool-1-thread-7]: Parser Instance:blog.techcypher.parser.impl.XmlParser@fcfa52
[pool-1-thread-9]: Parser Instance:blog.techcypher.parser.impl.XmlParser@fcfa52
[pool-1-thread-6]: Parser Instance:blog.techcypher.parser.impl.XmlParser@fcfa52
[pool-1-thread-10]: Parser Instance:blog.techcypher.parser.impl.XmlParser@fcfa52
Pool Stats:
Created:[1], Borrowed:[10]
显而易见地,解析器对象得以被动态创建和复用,bingo!
Commons Pool 2在性能和扩展性上都远胜Commons Pool 1。此外,Commons Pool 2还提供了鲁棒性绝佳的实例追踪和对象池监视。Commons Pool 2需要在JDK 1.6或更高版本下编译。还有许许多多的配置选项能用来管理池中对象的生命周期。
于是这篇长文也就到此为止了,希望能帮助到各位。;P
子曰:学而时习之,不亦说乎?