Backend Development 11 min read

Understanding HikariCP's ConcurrentBag: A Deep Dive into Lock‑Free Connection Pooling

This article explains how HikariCP's lock‑free ConcurrentBag manages database connections using ThreadLocal caches, a CopyOnWriteArrayList, AtomicInteger waiters, and a SynchronousQueue, and walks through the key code for borrowing and returning connections to improve Java multithreaded performance.

IT Services Circle
IT Services Circle
IT Services Circle
Understanding HikariCP's ConcurrentBag: A Deep Dive into Lock‑Free Connection Pooling

Reading a few hundred lines of HikariCP's source can greatly improve your Java concurrency skills; the library’s default connection pool uses a lock‑free data structure called ConcurrentBag to store database connections.

Core data structure

ConcurrentBag relies on four main fields: a CopyOnWriteArrayList named sharedList that caches all connections, a ThreadLocal list called threadList that holds per‑thread connections, an AtomicInteger waiters that counts threads waiting for a connection, and a zero‑capacity SynchronousQueue handoffQueue used for fast hand‑off.

private final CopyOnWriteArrayList
sharedList;
private final ThreadLocal
> threadList;
private final AtomicInteger waiters;
private final SynchronousQueue
handoffQueue;

sharedList caches all connections using a lock‑free list.

threadList caches connections used by the current thread, implemented as a ThreadLocal ArrayList .

waiters holds the number of threads currently trying to acquire a connection; it is an AtomicInteger .

handoffQueue is a SynchronousQueue that enables immediate transfer of a connection between threads.

The bag entries implement the following interface, which defines four possible states and CAS‑based state transitions:

public interface IConcurrentBagEntry {
    int STATE_NOT_IN_USE = 0;
    int STATE_IN_USE = 1;
    int STATE_REMOVED = -1;
    int STATE_RESERVED = -2;

    boolean compareAndSet(int expectState, int newState);
    void setState(int newState);
    int getState();
}

Acquiring a connection

The public borrow method obtains a connection, optionally with a timeout. It first tries the thread‑local list, then scans the shared list, and finally falls back to the handoff queue while tracking waiting threads.

public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException

Thread‑local lookup example:

// Try the thread‑local list first
final var list = threadList.get();
for (int i = list.size() - 1; i >= 0; i--) {
    final var 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;
    }
}

If the thread‑local cache is empty, the method increments waiters , scans sharedList for a free entry, and, if none is found, waits on handoffQueue with a nanosecond‑precision timeout, using parkNanos and Thread.yield() to avoid busy‑waiting.

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 var 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();
}

Returning a connection

When a thread finishes using a connection, the requite method resets its state and attempts to hand it back either to a waiting thread via the handoff queue or to the thread‑local cache.

public void requite(final T bagEntry) {
    bagEntry.setState(STATE_NOT_IN_USE);
    for (var i = 0; waiters.get() > 0; i++) {
        if (bagEntry.getState() != STATE_NOT_IN_USE || handoffQueue.offer(bagEntry)) {
            return;
        } else if ((i & 0xff) == 0xff) {
            parkNanos(MICROSECONDS.toNanos(10));
        } else {
            Thread.yield();
        }
    }
    final var threadLocalList = threadList.get();
    if (threadLocalList.size() < 50) {
        threadLocalList.add(weakThreadLocals ? new WeakReference<>(bagEntry) : bagEntry);
    }
}

The method first tries to place the connection into the handoff queue; if no waiter is present, it falls back to the per‑thread cache (up to 50 entries). It also uses a loop with parkNanos and Thread.yield() to avoid a tight CPU‑consuming loop when waiters stays high.

Key takeaways

Use ThreadLocal to cache resources per thread and reduce lock contention.

Employ a read‑heavy, write‑light CopyOnWriteArrayList for storing all objects.

Leverage AtomicInteger and CAS for lock‑free counting of waiting threads.

Utilize a zero‑capacity SynchronousQueue for fast hand‑off between threads.

Control state transitions with compareAndSet CAS primitives.

Combine parkNanos and Thread.yield() in loops to prevent CPU‑burning busy‑waits.

Understand the differences between offer , poll , peek , put , take , add , and remove in concurrent collections.

Apply the volatile keyword correctly when using CAS for visibility guarantees.

Know how WeakReference behaves during garbage collection.

Mastering these concepts in the relatively small ConcurrentBag implementation can dramatically improve your ability to write efficient multithreaded Java code.

JavaConcurrencyConnection PoolHikariCPthreadlocallock-freeConcurrentBag
IT Services Circle
Written by

IT Services Circle

Delivering cutting-edge internet insights and practical learning resources. We're a passionate and principled IT media platform.

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.