Building a JDBC Connection Pool with Java Concurrency Primitives
A lightweight, in-memory connection pool can be implemented with nothing more than java.sql.Connection stubs, a LinkedList, and the classic wait/notify mechanism. Below you’ll find a complete walk-through that covers stub generation, pool logic, timeout handdling, and a stress-test harness.
- Stubbing a JDBC Connection
Because we only need a placeholder object that implements java.sql.Connection, a dynamic proxy is the simplest approach:
package pool.stub;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
public final class StubFactory {
private static class NoOpHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
// Every method returns null; real pools would delegate to a driver.
return null;
}
}
public static Connection newStub() {
return (Connection) Proxy.newProxyInstance(
StubFactory.class.getClassLoader(),
new Class>[]{Connection.class},
new NoOpHandler());
}
}
- Core Pool Implementation
The pool keeps available stubs in a LinkedList. All access is guarded by the list’s intrinsic lock to ensure visibility and mutual exclusion.
package pool.core;
import pool.stub.StubFactory;
import java.sql.Connection;
import java.util.LinkedList;
public final class SimplePool {
private final LinkedList<Connection> free = new LinkedList<>();
public SimplePool(int initialSize) {
for (int i = 0; i < initialSize; i++) {
free.addLast(StubFactory.newStub());
}
}
/**
* Returns a stub to the pool and wakes up any waiting threads.
*/
public void release(Connection c) {
if (c == null) return;
synchronized (free) {
free.addLast(c);
free.notifyAll();
}
}
/**
* Tries to obtain a stub within the given timeout.
* @param timeoutMillis 0 means infinite wait.
*/
public Connection acquire(long timeoutMillis) throws InterruptedException {
synchronized (free) {
if (timeoutMillis <= 0) {
while (free.isEmpty()) {
free.wait();
}
return free.removeFirst();
}
long deadline = System.currentTimeMillis() + timeoutMillis;
long remaining = timeoutMillis;
while (free.isEmpty() && remaining > 0) {
free.wait(remaining);
remaining = deadline - System.currentTimeMillis();
}
return free.isEmpty() ? null : free.removeFirst();
}
}
}
- Stress-Testing the Pool
We simulate 1 000 concurrent threads that each try to borrow and return a stub 20 times. Two CountDownLatch objects are used:
startGate– released once all threads are ready, ensuring they hit the pool at the same moment.endGate– counts down every time a thread finishes, letting the main thread await completion.
package pool.test;
import pool.core.SimplePool;
import java.sql.Connection;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
public final class PoolStressTest {
private static final SimplePool POOL = new SimplePool(10);
public static void main(String[] args) throws InterruptedException {
final int threads = 1000;
final int iterations = 20;
final CountDownLatch startGate = new CountDownLatch(1);
final CountDownLatch endGate = new CountDownLatch(threads);
final AtomicInteger acquired = new AtomicInteger();
final AtomicInteger timedOut = new AtomicInteger();
for (int i = 0; i < threads; i++) {
new Thread(() -> {
try {
startGate.await(); // wait for the starting gun
for (int j = 0; j < iterations; j++) {
Connection c = POOL.acquire(1); // 1 ms timeout
if (c != null) {
try {
c.createStatement(); // pretend to use it
} finally {
POOL.release(c);
acquired.incrementAndGet();
}
} else {
timedOut.incrementAndGet();
}
}
} catch (InterruptedException ignored) {
} finally {
endGate.countDown();
}
}).start();
}
startGate.countDown(); // fire!
endGate.await(); // wait for everyone to finish
System.out.println("Total attempts: " + (threads * iterations));
System.out.println("Successful borrows: " + acquired.get());
System.out.println("Timeouts: " + timedOut.get());
}
}
Typical Output
Total attempts: 20000
Successful borrows: 11914
Timeouts: 8086
Raising the timeout to 100 ms shifts the balance dramatically:
Total attempts: 20000
Successful borrows: 19050
Timeouts: 950
The experiment shows how wait/notify plus timeout parameters can be tuned to trade latency against availability in a hand-rolled JDBC connection pool.