Fundamentals 19 min read

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.

Sohu Tech Products
Sohu Tech Products
Sohu Tech Products
Understanding Apache Commons Pool2: Core Interfaces, Object States, Configuration Options, and Practical Usage

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

JavaConcurrencyResource ManagementObject poolApache Commons Pool2
Sohu Tech Products
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.