Mastering Java Connection Pools: From Commons Pool to HikariCP Performance Secrets
This article explains why pooling costly resources like database connections improves performance, introduces the Commons Pool 2 library and its core classes, demonstrates a Jedis pool with JMH benchmarks showing a 5× throughput boost, and dives into HikariCP's optimizations, configuration tips, and common interview questions.
Preface
In typical coding we cache costly objects such as thread resources, database connections, or TCP connections because their creation is expensive; frequent creation and destruction waste system resources and degrade performance. A lightweight reset allows these objects to be reused via a virtual pool.
Commons Pool 2
Overview
Commons Pool 2 is a widely used pooling library in Java. The core class GenericObjectPool creates object pools by receiving a configuration and a factory.
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.11.1</version>
</dependency>The constructor signature is:
public GenericObjectPool(final PooledObjectFactory<T> factory,
final GenericObjectPoolConfig<T> config)Case Study: Jedis
Jedis, a popular Redis client, uses Commons Pool to manage connections. The factory’s makeObject method returns a PooledObject wrapped with new DefaultPooledObject<>(obj).
@Override
public PooledObject<Jedis> makeObject() throws Exception {
Jedis jedis = null;
try {
jedis = new Jedis(jedisSocketFactory, clientConfig);
jedis.connect();
return new DefaultPooledObject<>(jedis);
} catch (JedisException je) {
if (jedis != null) {
try { jedis.quit(); } catch (RuntimeException e) { logger.warn("Error while QUIT", e); }
try { jedis.close(); } catch (RuntimeException e) { logger.warn("Error while close", e); }
}
throw je;
}
}Borrowing an object works as follows:
public T borrowObject(final Duration borrowMaxWaitDuration) throws Exception {
// omitted lines
while (p == null) {
create = false;
// first try to get from idle queue
p = idleObjects.pollFirst();
// if none, create a new one via factory
if (p == null) {
p = create();
if (p != null) { create = true; }
}
// omitted lines
}
// omitted lines
}The pool stores objects in a LinkedBlockingDeque, a double‑ended queue.
Key configuration properties of GenericObjectPoolConfig include:
private int maxTotal = DEFAULT_MAX_TOTAL;
private int maxIdle = DEFAULT_MAX_IDLE;
private int minIdle = DEFAULT_MIN_IDLE;
private boolean lifo = DEFAULT_LIFO;
private boolean fairness = DEFAULT_FAIRNESS;
private long maxWaitMillis = DEFAULT_MAX_WAIT_MILLIS;
private long minEvictableIdleTimeMillis = DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS;
private long softMinEvictableIdleTimeMillis = DEFAULT_SOFT_MIN_EVICTABLE_IDLE_TIME_MILLIS;
private int numTestsPerEvictionRun = DEFAULT_NUM_TESTS_PER_EVICTION_RUN;
private EvictionPolicy<T> evictionPolicy = null;
private String evictionPolicyClassName = DEFAULT_EVICTION_POLICY_CLASS_NAME;
private boolean testOnCreate = DEFAULT_TEST_ON_CREATE;
private boolean testOnBorrow = DEFAULT_TEST_ON_BORROW;
private boolean testOnReturn = DEFAULT_TEST_ON_RETURN;
private boolean testWhileIdle = DEFAULT_TEST_WHILE_IDLE;
private long timeBetweenEvictionRunsMillis = DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS;
private boolean blockWhenExhausted = DEFAULT_BLOCK_WHEN_EXHAUSTED;Typical interview question: how large should the timeout be? A practical answer is to set maxWaitMillis to the maximum latency the interface can tolerate, e.g., 500‑1000 ms, causing a NoSuchElementException on timeout for fast failure.
JMH Benchmark
A simple JMH test compares pooled vs. non‑pooled Redis access, showing roughly a 5× throughput increase when using a pool.
@Fork(2)
@State(Scope.Benchmark)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 1)
@BenchmarkMode(Mode.Throughput)
public class JedisPoolVSJedisBenchmark {
JedisPool pool = new JedisPool("localhost", 6379);
@Benchmark
public void testPool() {
Jedis jedis = pool.getResource();
jedis.set("a", UUID.randomUUID().toString());
jedis.close();
}
@Benchmark
public void testJedis() {
Jedis jedis = new Jedis("localhost", 6379);
jedis.set("a", UUID.randomUUID().toString());
jedis.close();
}
}Database Connection Pool HikariCP
HikariCP, the default SpringBoot connection pool, achieves near‑light‑speed performance. Its advantages stem from three techniques:
Uses FastList instead of ArrayList to reduce bounds checks.
Optimizes bytecode with Javassist, replacing invokevirtual with invokestatic.
Implements a lock‑free ConcurrentBag to lower contention.
Key pool size parameters are maximumPoolSize and minimumIdle. In practice, 20‑50 database connections are sufficient; setting the pool too large wastes resources.
HikariCP does not expose the many test* flags; instead it relies on Connection.isValid() for health checks.
Result Cache Pool
The concept of a result cache pool combines pooling and caching: processed objects are stored in a fast‑access area, similar to how JSP compiles to class files or static HTML pages are generated for high‑traffic content.
Summary
When objects are costly to create or destroy, have long initialization times, and can be reset for reuse, consider pooling. Adjust key parameters such as pool size and timeout to maximize performance, and monitor pool metrics to detect issues like connection leaks.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java High-Performance Architecture
Sharing Java development articles and resources, including SSM architecture and the Spring ecosystem (Spring Boot, Spring Cloud, MyBatis, Dubbo, Docker), Zookeeper, Redis, architecture design, microservices, message queues, Git, etc.
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.
