Understanding HikariCP: Architecture, Core Components, and Usage
This article explains how HikariCP, the default Spring Boot 2.x connection pool, improves database performance through byte‑code optimization, custom containers, and streamlined code, and it details the internal classes, lifecycle methods, configuration, monitoring, and practical usage examples.
1. Introduction
Database connection pools cache a fixed number of connections to avoid the overhead of creating and destroying connections for each request. HikariCP, the default pool in Spring Boot 2.x, claims to be the fastest Java pool due to several low‑level optimizations.
Byte‑code size reduction (methods kept under 35 bytecodes)
Custom ConcurrentBag container
Custom FastList implementation
Compact code base compared with Druid
The following sections describe the main classes ( HikariDataSource , HikariPool , ConcurrentBag , PoolEntry ) and the internal workflow of HikariCP (version 2.7.9).
2. Overall Description
2.1 Class Relationship Diagram
HikariDataSource implements the DataSource interface and holds the pool configuration; HikariConfig validates the parameters.
2.2 Overall Process Diagram
3. HikariPool
The class com.zaxxer.hikari.pool.HikariPool manages pool initialization, scaling, leak detection and monitoring.
/**
* Construct a HikariPool with the specified configuration.
*/
public HikariPool(final HikariConfig config) {
// use configuration to create a DataSource for real connections
super(config);
// custom concurrent bag for high‑performance storage
this.connectionBag = new ConcurrentBag<>(this);
this.suspendResumeLock = config.isAllowPoolSuspension() ? new SuspendResumeLock() : SuspendResumeLock.FAUX_LOCK;
this.houseKeepingExecutorService = initializeHouseKeepingExecutorService();
checkFailFast();
if (config.getMetricsTrackerFactory() != null) {
setMetricsTrackerFactory(config.getMetricsTrackerFactory());
} else {
setMetricRegistry(config.getMetricRegistry());
}
setHealthCheckRegistry(config.getHealthCheckRegistry());
registerMBeans(this);
ThreadFactory threadFactory = config.getThreadFactory();
LinkedBlockingQueue
addConnectionQueue = new LinkedBlockingQueue<>(config.getMaximumPoolSize());
this.addConnectionQueue = unmodifiableCollection(addConnectionQueue);
this.addConnectionExecutor = createThreadPoolExecutor(addConnectionQueue, poolName + " connection adder", threadFactory, new ThreadPoolExecutor.DiscardPolicy());
this.closeConnectionExecutor = createThreadPoolExecutor(config.getMaximumPoolSize(), poolName + " connection closer", threadFactory, new ThreadPoolExecutor.CallerRunsPolicy());
this.leakTaskFactory = new ProxyLeakTaskFactory(config.getLeakDetectionThreshold(), houseKeepingExecutorService);
this.houseKeeperTask = houseKeepingExecutorService.scheduleWithFixedDelay(new HouseKeeper(), 100L, HOUSEKEEPING_PERIOD_MS, MILLISECONDS);
}After initialization, connections are stored in a ConcurrentBag , which is the actual container for physical connections.
4. ConcurrentBag
ConcurrentBag is a generic resource pool used by HikariCP to store connections (or any object implementing IConcurrentBagEntry ).
4.1 IConcurrentBagEntry
Defines four states:
State
Description
STATE_NOT_IN_USE
Idle
STATE_IN_USE
In use
STATE_REMOVED
Removed
STATE_RESERVED
Reserved (e.g., during shrink)
4.2 PoolEntry
final class PoolEntry implements IConcurrentBagEntry {
private static final Logger LOGGER = LoggerFactory.getLogger(PoolEntry.class);
private static final AtomicIntegerFieldUpdater
stateUpdater;
Connection connection;
long lastAccessed;
long lastBorrowed;
private volatile int state = 0;
private volatile boolean evict;
private volatile ScheduledFuture
endOfLife;
private final FastList
openStatements;
private final HikariPool hikariPool;
private final boolean isReadOnly;
private final boolean isAutoCommit;
}PoolEntry wraps a physical JDBC connection and stores metadata such as timestamps, eviction flag, and open statements.
4.3 Key Properties of ConcurrentBag
sharedList – holds all resources.
weakThreadLocals – whether thread‑local entries are stored as WeakReference .
threadList – thread‑local cache of resources.
waiters – number of threads waiting for a resource.
closed – pool closed flag.
handoffQueue – a synchronous queue used to block threads when no idle resource is available.
4.4 Core Methods
4.4.1 borrow
Attempts to obtain an idle entry from the thread‑local list, then from the shared list, and finally blocks on handoffQueue if necessary.
public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException {
final List
list = threadList.get();
for (int i = list.size() - 1; i >= 0; i--) {
final Object entry = list.remove(i);
final T bagEntry = weakThreadLocals ? ((WeakReference
) entry).get() : (T) entry;
if (bagEntry != null && bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
return bagEntry;
}
}
final int waiting = waiters.incrementAndGet();
try {
for (T bagEntry : sharedList) {
if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
if (waiting > 1) {
listener.addBagItem(waiting - 1);
}
return bagEntry;
}
}
listener.addBagItem(waiting);
timeout = timeUnit.toNanos(timeout);
do {
final long start = currentTime();
final T bagEntry = handoffQueue.poll(timeout, NANOSECONDS);
if (bagEntry == null || bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
return bagEntry;
}
timeout -= elapsedNanos(start);
} while (timeout > 10_000);
return null;
} finally {
waiters.decrementAndGet();
}
}4.4.2 add
Adds a new idle entry to sharedList and wakes waiting threads.
public void add(final T bagEntry) {
if (closed) {
LOGGER.info("ConcurrentBag has been closed, ignoring add()");
throw new IllegalStateException("ConcurrentBag has been closed, ignoring add()");
}
sharedList.add(bagEntry);
while (waiters.get() > 0 && !handoffQueue.offer(bagEntry)) {
yield();
}
}4.4.3 requite
Returns a resource to the pool, setting its state to STATE_NOT_IN_USE and possibly handing it to waiting threads.
public void requite(final T bagEntry) {
bagEntry.setState(STATE_NOT_IN_USE);
for (int i = 0; waiters.get() > 0; i++) {
if (bagEntry.getState() != STATE_NOT_IN_USE || handoffQueue.offer(bagEntry)) {
return;
}
if ((i & 0xff) == 0xff) {
parkNanos(MICROSECONDS.toNanos(10));
} else {
yield();
}
}
final List
threadLocalList = threadList.get();
threadLocalList.add(weakThreadLocals ? new WeakReference<>(bagEntry) : bagEntry);
}4.4.4 remove
Removes a borrowed or reserved entry from the pool.
public boolean remove(final T bagEntry) {
if (!bagEntry.compareAndSet(STATE_IN_USE, STATE_REMOVED) &&
!bagEntry.compareAndSet(STATE_RESERVED, STATE_REMOVED) &&
!closed) {
LOGGER.warn("Attempt to remove an object from the bag that was not borrowed or reserved: {}", bagEntry);
return false;
}
final boolean removed = sharedList.remove(bagEntry);
if (!removed && !closed) {
LOGGER.warn("Attempt to remove an object from the bag that does not exist: {}", bagEntry);
}
return removed;
}4.4.5 reserve / unreserve
Used mainly during pool shrinkage to temporarily protect a connection.
public boolean reserve(final T bagEntry) {
return bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_RESERVED);
} public void unreserve(final T bagEntry) {
if (bagEntry.compareAndSet(STATE_RESERVED, STATE_NOT_IN_USE)) {
while (waiters.get() > 0 && !handoffQueue.offer(bagEntry)) {
Thread.yield();
}
} else {
LOGGER.warn("Attempt to relinquish an object to the bag that was not reserved: {}", bagEntry);
}
}5. Getting a Connection
The pool lazily initializes on the first request, validates configuration, acquires the suspendResumeLock , and then calls connectionBag.borrow(timeout, MILLISECONDS) . If the timeout expires, null is returned and an exception is thrown.
6. Closing a Connection
void closeConnection(final PoolEntry poolEntry, final String closureReason) {
if (connectionBag.remove(poolEntry)) {
final Connection connection = poolEntry.close();
closeConnectionExecutor.execute(() -> {
quietlyCloseConnection(connection, closureReason);
if (poolState == POOL_NORMAL) {
fillPool();
}
});
}
}The removal is asynchronous to keep the main thread lightweight.
7. Pool Expansion
private synchronized void fillPool() {
final int connectionsToAdd = Math.min(config.getMaximumPoolSize() - getTotalConnections(),
config.getMinimumIdle() - getIdleConnections())
- addConnectionQueue.size();
for (int i = 0; i < connectionsToAdd; i++) {
addConnectionExecutor.submit((i < connectionsToAdd - 1) ? POOL_ENTRY_CREATOR : POST_FILL_POOL_ENTRY_CREATOR);
}
}The method ensures that the pool maintains at least minimumIdle idle connections while respecting maximumPoolSize .
8. Using HikariCP in Spring Boot
Typical YAML configuration for a MySQL datasource:
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/demo_db?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: ENC(iTgGY9Zl+EqWRfyqfRN54/54nTvXIAsY)
hikari:
maximum-pool-size: 10
idle-timeout: 180000
auto-commit: true
pool-name: defaultHikariPool
max-lifetime: 1800000
connection-timeout: 30000
connection-test-query: SELECT 1New Oriental Technology
Practical internet development experience, tech sharing, knowledge consolidation, and forward-thinking insights.
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.