Introduction to Object Pooling
In software architecture, managing resources that require significant time to instantiate or terminate is a critical optimization challenge. Techniques known as pooling address this by maintaining a collection of reusable instances, thereby mitigating system overhead and enhancing throughput. Common applications include database connections, thread pools, and caching layers like Redis.
Apache Commons Pool 2 is a widely adopted library providing a robust framework for generic object pooling. It abstracts the complexity of lifecycle management, allowing developers to focus on business logic while the library ensures efficient allocation and validation of pooled resources.
Integrasion and Setup
To begin utilizing the library, the appropriate dependency must be included in the build configuration (e.g., Maven).
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.8.1</version>
</dependency>
Defining the Lifecycle Factory
The core component is the PooledObjectFactory interface. It dictates how instances are created, validated, activated, deactivated, and destroyed. The factory serves as the bridge between the pool manager and the actual resourcce instances.
public interface PooledObjectFactory<T> {
PooledObject<T> makeObject() throws Exception;
void destroyObject(PooledObject<T> p) throws Exception;
boolean validateObject(PooledObject<T> p);
void activateObject(PooledObject<T> p) throws Exception;
void passivateObject(PooledObject<T> p) throws Exception;
}
Consider an application requiring a managed resource. We define the entity and its lifecycle behaviors:
public class ManagedEntity {
private final String uniqueIdentifier = UUID.randomUUID().toString();
private volatile boolean operational = true;
public void initialize() {
System.out.println("Initializing resource instance: " + uniqueIdentifier);
operational = true;
}
public void teardown() {
System.out.println("Cleaning up resource: " + uniqueIdentifier);
operational = false;
}
public boolean isActive() {
return operational;
}
public String getId() {
return uniqueIdentifier;
}
}
Next, we implement the factory logic:
public class EntityFactory implements PooledObjectFactory<ManagedEntity> {
@Override
public PooledObject<ManagedEntity> makeObject() throws Exception {
ManagedEntity resource = new ManagedEntity();
resource.initialize();
return new DefaultPooledObject<>(resource);
}
@Override
public void destroyObject(PooledObject<ManagedEntity> p) throws Exception {
p.getObject().teardown();
}
@Override
public boolean validateObject(PooledObject<ManagedEntity> p) {
return p.getObject().isActive();
}
@Override
public void activateObject(PooledObject<ManagedEntity> p) throws Exception {
// Pre-use initialization logic
}
@Override
public void passivateObject(PooledObject<ManagedEntity> p) throws Exception {
// Post-return cleanup logic
}
}
Configuring and Using the Pool
Once the factory is ready, configure the pool parameters such as maximum capacity and idle time settings. Then, retrieve and release objects using the standard try-finally pattern to ensure resource safety.
GenericObjectPoolConfig<ManagedEntity> config = new GenericObjectPoolConfig<>();
config.setMaxTotal(50);
config.setMaxIdle(10);
config.setTestWhileIdle(true);
config.setMinEvictableIdleTimeMillis(60000L);
GenericObjectPool<ManagedEntity> resourcePool =
new GenericObjectPool<>(new EntityFactory(), config);
Usage involves borrowing the resource and returning it asynchronously:
ManagedEntity entity = null;
try {
entity = resourcePool.borrowObject();
System.out.println("Acquired resource: " + entity.getId() +
" on thread: " + Thread.currentThread().getName());
} catch (Exception e) {
e.printStackTrace();
} finally {
if (entity != null) {
try {
resourcePool.returnObject(entity);
} catch (Exception e) {
e.printStackTrace();
}
}
}
Real World Application: Redis Connectivity
A practical scenario involves using the Jedis library to connect to Redis databases. Internally, Jedis leverages Commons Pool to manage TCP connections. Below is an example demonstrating connection acquisition and release.
public class RedisConnectionDemo {
public static void main(String[] args) throws InterruptedException {
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(100);
poolConfig.setMaxIdle(25);
JedisPool connectionPool = new JedisPool(poolConfig, "127.0.0.1", 6379);
Jedis jedis = null;
try {
jedis = connectionPool.getResource();
jedis.set("demo_key", "Active Data");
String data = jedis.get("demo_key");
System.out.println("Retrieved: " + data);
} finally {
if (jedis != null) {
jedis.close();
}
}
Thread.sleep(5000);
connectionPool.close();
}
}
Under the Hood: Mechanics
Understanding the internal behavior of GenericObjectPool is crucial for debguging and tuning. Initialization primarily sets up the factory reference and creates the storage containers. Specifically, the idle objects are maintained in a LinkedBlockingDeque, ensuring FIFO order with concurrent access capabilities. A secondary map tracks all active or idle instances globally for administrative purposes.
When borrowObject is invoked:
- The pool attempts to fetch an existing instance from the idle deque.
- If none are available, the factory creates a new instance.
- Upon successful retrieval or creation, validation logic runs, and the object is activated before being handed to the caller.
Conversely, returnObject handles the lifecycle transition back to the pool:
- Validation checks occur based on configuration.
- If the count of idle objects exceeds the configured maximum, the returned instance may be destroyed immediately to prevent resource leaks.
- If validation passes, the object is passedivated and added back to the idle container for future reuse.