Understanding Apache Commons Pool2: Core Interfaces, Object States, Configuration Options, and Practical Usage
This article explains the concept of object pools, introduces Apache Commons Pool2 core interfaces and object states, details configuration parameters, describes the borrowing and returning workflow, and provides a complete Java example of a simple string pool implementation.
Introduction
An object pool is a container that stores reusable objects, similar to thread pools or database connection pools, following the pooling design pattern. It reduces the overhead of frequent object creation and destruction, improves reuse, saves resources, and eases the JVM garbage collector's load.
Apache Commons Pool2 is a generic object pool implementation provided by Apache, allowing easy customization of pools; for example, the Redis client Jedis uses it internally.
Core Interfaces
The main internal classes of Apache Commons Pool2 are:
ObjectPool : the pool interface used to obtain and return objects.
PooledObject : wraps the actual object and stores metadata such as creation time and last used time.
PooledObjectFactory : manages the lifecycle of pooled objects, providing creation, destruction, validation, activation, and passivation.
BaseObjectPoolConfig : base configuration class (e.g., FIFO, test on create). GenericObjectPoolConfig extends it with default settings.
KeyedObjectPool<K,V> and KeyedPooledObjectFactory<K,V> : rarely used keyed pool interfaces.
Object Pool States
The PooledObjectState enum defines all possible states of a pooled object:
public enum PooledObjectState {
IDLE, // in idle queue, not used
ALLOCATED, // currently in use
EVICTION, // idle, being tested for eviction
EVICTION_RETURN_TO_HEAD,
VALIDATION,
VALIDATION_PREALLOCATED,
VALIDATION_RETURN_TO_HEAD,
INVALID,
ABANDONED,
RETURNING
}For example, an object becomes ABANDONED when it stays allocated longer than the configured removeAbandonedTimeout .
private void removeAbandoned(final AbandonedConfig abandonedConfig) {
final long now = System.currentTimeMillis();
final long timeout = now - (abandonedConfig.getRemoveAbandonedTimeout() * 1000L);
final ArrayList
> remove = new ArrayList<>();
final Iterator
> it = allObjects.values().iterator();
while (it.hasNext()) {
final PooledObject
pooledObject = it.next();
synchronized (pooledObject) {
if (pooledObject.getState() == PooledObjectState.ALLOCATED &&
pooledObject.getLastUsedTime() <= timeout) {
pooledObject.markAbandoned();
remove.add(pooledObject);
}
}
}
}Borrowing Process
The steps to obtain an object are:
Check AbandonedConfig and clean up abandoned objects if necessary.
Try to poll an object from the idle queue; if none, create a new one.
If an object is found, activate it via factory.activateObject .
If testOnBorrow is true, validate the object with factory.validateObject . On failure, destroy the object and possibly throw an exception.
Update borrowing statistics and return the underlying object.
public T borrowObject(final long borrowMaxWaitMillis) throws Exception {
assertOpen();
final AbandonedConfig ac = this.abandonedConfig;
if (ac != null && ac.getRemoveAbandonedOnBorrow() &&
(getNumIdle() < 2) && (getNumActive() > getMaxTotal() - 3)) {
removeAbandoned(ac);
}
PooledObject
p = null;
final boolean blockWhenExhausted = getBlockWhenExhausted();
final long waitTime = System.currentTimeMillis();
while (p == null) {
boolean create = false;
p = idleObjects.pollFirst();
if (p == null) {
p = create();
if (p != null) create = true;
}
if (blockWhenExhausted) {
if (p == null) {
if (borrowMaxWaitMillis < 0) {
p = idleObjects.takeFirst();
} else {
p = idleObjects.pollFirst(borrowMaxWaitMillis, TimeUnit.MILLISECONDS);
}
}
if (p == null) {
throw new NoSuchElementException("Timeout waiting for idle object");
}
} else {
if (p == null) {
throw new NoSuchElementException("Pool exhausted");
}
}
if (!p.allocate()) {
p = null;
}
if (p != null) {
try {
factory.activateObject(p);
} catch (Exception e) {
try { destroy(p, DestroyMode.NORMAL); } catch (Exception ignored) {}
p = null;
if (create) {
NoSuchElementException nsee = new NoSuchElementException("Unable to activate object");
nsee.initCause(e);
throw nsee;
}
}
if (p != null && getTestOnBorrow()) {
boolean validate = false;
Throwable validationThrowable = null;
try {
validate = factory.validateObject(p);
} catch (Throwable t) {
PoolUtils.checkRethrow(t);
validationThrowable = t;
}
if (!validate) {
try { destroy(p, DestroyMode.NORMAL); destroyedByBorrowValidationCount.incrementAndGet(); } catch (Exception ignored) {}
p = null;
if (create) {
NoSuchElementException nsee = new NoSuchElementException("Unable to validate object");
nsee.initCause(validationThrowable);
throw nsee;
}
}
}
}
}
updateStatsBorrow(p, System.currentTimeMillis() - waitTime);
return p.getObject();
}Activation and Passivation
When returning an object, the pool calls factory.passivateObject . If validation fails, the object is destroyed; otherwise, it is passivated and placed back into the idle queue. Activation occurs before an object is handed out.
public void returnObject(final T obj) {
final PooledObject
p = allObjects.get(new IdentityWrapper<>(obj));
try {
factory.passivateObject(p);
} catch (Exception e1) {
swallowException(e1);
try { destroy(p, DestroyMode.NORMAL); } catch (Exception e) { swallowException(e); }
try { ensureIdle(1, false); } catch (Exception e) { swallowException(e); }
updateStatsReturn(activeTime);
return;
}
// add back to idle queue …
}Configuration Options
Key configuration items of GenericObjectPool (via GenericObjectPoolConfig ) include:
maxTotal : maximum total objects (default 8).
maxIdle : maximum idle objects (default 8).
minIdle : minimum idle objects (default 0).
lifo : use LIFO ordering for idle objects (default true ).
blockWhenExhausted : block when pool is exhausted (default true ).
fairness : fair lock for waiting threads (default false ).
maxWaitMillis : maximum wait time for a borrowed object (default -1 = indefinite).
testOnCreate , testOnBorrow , testOnReturn : validation flags.
timeBetweenEvictionRunsMillis and testWhileIdle : eviction settings.
Usage Steps
Create a factory class by extending BaseGenericObjectPool or implementing PooledObjectFactory , overriding creation, destruction, validation, activation, and passivation.
Create the pool using GenericObjectPool (recommended) or directly implementing ObjectPool .
Best Practices
Adjust maxIdle and minIdle according to workload; the default of 8 may be insufficient.
Set maxWaitMillis to avoid indefinite blocking.
Always return objects to the pool in a finally block, even when exceptions occur.
try {
item = pool.borrowObject();
// use item
} catch (Exception e) {
log.error("...", e);
} finally {
pool.returnObject(item);
}Example: Simple String Pool
String Factory
package com.anqi.demo.demopool2.pool.fac;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
public class StringPoolFac extends BasePooledObjectFactory
{
@Override
public String create() throws Exception {
return "str-val-";
}
@Override
public PooledObject
wrap(String s) {
return new DefaultPooledObject<>(s);
}
@Override
public void destroyObject(PooledObject
p) throws Exception {}
@Override
public boolean validateObject(PooledObject
p) {
return super.validateObject(p);
}
@Override
public void activateObject(PooledObject
p) throws Exception {
super.activateObject(p);
}
@Override
public void passivateObject(PooledObject
p) throws Exception {
super.passivateObject(p);
}
}String Pool
package com.anqi.demo.demopool2.pool;
import org.apache.commons.pool2.PooledObjectFactory;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
public class StringPool extends GenericObjectPool
{
public StringPool(PooledObjectFactory
factory) {
super(factory);
}
public StringPool(PooledObjectFactory
factory, GenericObjectPoolConfig
config) {
super(factory, config);
}
}Test Main
import com.anqi.demo.demopool2.pool.fac.StringPoolFac;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class StringPoolTest {
private static final Logger LOG = LoggerFactory.getLogger(StringPoolTest.class);
public static void main(String[] args) {
StringPoolFac fac = new StringPoolFac();
GenericObjectPoolConfig
config = new GenericObjectPoolConfig<>();
config.setMaxTotal(2);
config.setMinIdle(1);
config.setMaxWaitMillis(3000);
StringPool pool = new StringPool(fac, config);
for (int i = 0; i < 3; i++) {
String s = "";
try {
s = pool.borrowObject();
LOG.info("str:{}", s);
} catch (Exception e) {
e.printStackTrace();
} finally {
// Uncomment the following line to return the object and avoid blocking:
// if (!s.equals("")) pool.returnObject(s);
}
}
}
}Running the test without returning objects results in two successful borrows followed by a 3‑second block and a NoSuchElementException . When the return line is enabled, all three borrows succeed.
Full source code: https://github.com/Motianshi/alldemo/tree/master/demo-pool2
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.