Thread Pools, Netty, Tomcat & HikariCP
- New learners โ start at What is a Thread Pool? to understand why pooling exists and how it works internally.
- Intermediate โ jump to Tomcat or Netty to understand the server layer.
- Senior engineers โ see How They All Relate, Production Sizing, and Troubleshooting.
1. Thread Pools โ ThreadPoolExecutorโ
What is a Thread Pool?โ
A thread pool is a managed collection of pre-created threads that are reused to execute tasks. Instead of creating a new OS thread for every task (expensive: ~1MB stack + kernel call), the pool maintains a fixed number of threads that pick tasks from a queue.
Without a pool:
Task 1 โ create Thread โ run โ destroy Thread
Task 2 โ create Thread โ run โ destroy Thread
Task 3 โ create Thread โ run โ destroy Thread
Cost: 3 ร (1ms create + 1ms destroy) = 6ms overhead
With a pool:
Pool: [Thread-1] [Thread-2] [Thread-3] โ pre-created, reused
Task 1 โ Thread-1 picks it up โ runs โ Thread-1 returns to pool
Task 2 โ Thread-2 picks it up โ runs โ Thread-2 returns to pool
Cost: 0ms overhead (threads already exist)
How ThreadPoolExecutor Works Internallyโ
java.util.concurrent.ThreadPoolExecutor is the engine behind all Java thread pools. Understanding its internals prevents catastrophic production failures.
Constructor Parametersโ
ThreadPoolExecutor executor = new ThreadPoolExecutor(
4, // corePoolSize
8, // maximumPoolSize
60, TimeUnit.SECONDS, // keepAliveTime
new ArrayBlockingQueue<>(100), // workQueue
new ThreadFactory() { ... }, // threadFactory
new CallerRunsPolicy() // rejectionHandler
);
| Parameter | What It Controls | Why It Matters |
|---|---|---|
corePoolSize | Threads that stay alive even when idle | Too low โ tasks queue; too high โ wasted RAM |
maximumPoolSize | Absolute thread ceiling under burst load | Safety valve โ prevents unbounded thread creation |
keepAliveTime | How long non-core threads survive idle | Lets burst threads die after the spike passes |
workQueue | Buffer for tasks when all core threads are busy | Bounded = backpressure; Unbounded = OOM risk |
threadFactory | Custom thread naming and daemon settings | Named threads = readable thread dumps |
rejectionHandler | What happens when pool AND queue are full | CallerRunsPolicy = natural backpressure |
Task Submission Flow (Critical for Interviews)โ
Task submitted to ThreadPoolExecutor
โ
โโโ Are there idle core threads?
โ YES โ Assign task to an idle core thread
โ NO โ
โ
โโโ Is the work queue full?
โ NO โ Add task to the queue (waits for a thread)
โ YES โ
โ
โโโ Is maximumPoolSize reached?
โ NO โ Create a new non-core thread to handle the task
โ YES โ
โ
โโโ Execute the RejectionPolicy
โโโ AbortPolicy (default): throw RejectedExecutionException
โโโ CallerRunsPolicy: run task in the caller's thread โ backpressure
โโโ DiscardPolicy: silently drop the task
โโโ DiscardOldestPolicy: drop oldest queued task, retry
Visual timeline:
corePoolSize=2, maxPoolSize=4, queue=3
Tasks: T1 T2 T3 T4 T5 T6 T7 T8
โ โ โ โ โ โ โ โ
Core: [T1][T2] โ Core threads handle T1, T2
Queue: [T3][T4][T5] โ Queue absorbs T3โT5
Non-core: [T6][T7] โ Non-core threads created for T6, T7
Reject: [T8] โ RejectionPolicy triggered
Executors Factory Methods Are Dangerous| Factory Method | Hidden Danger |
|---|---|
Executors.newFixedThreadPool(n) | Uses unbounded LinkedBlockingQueue โ tasks pile up โ OOM |
Executors.newCachedThreadPool() | maximumPoolSize = Integer.MAX_VALUE โ creates unlimited threads โ OOM |
Executors.newSingleThreadExecutor() | Unbounded queue โ same OOM risk as fixed pool |
Always use ThreadPoolExecutor directly with bounded queues in production.
Sizing Thread Poolsโ
CPU-Bound Tasksโ
Tasks that compute without waiting (sorting, encryption, JSON parsing):
Optimal threads = CPU_cores + 1
Why +1? Insurance against page faults. If one thread stalls on
a memory page fault, the extra thread keeps the CPU busy.
Example: 8-core server โ 9 threads for CPU-bound work
I/O-Bound Tasksโ
Tasks that spend most of their time waiting (DB queries, HTTP calls, file reads):
Optimal threads = CPU_cores ร target_utilization ร (1 + wait_time / compute_time)
Example:
8 cores, 70% CPU target
Average HTTP call: 200ms wait, 2ms compute
= 8 ร 0.7 ร (1 + 200/2) = 8 ร 0.7 ร 101 = 565 threads
Rule of thumb: if tasks are 99% waiting, you need ~100ร more
threads than cores to keep the CPU busy.
Spring Boot @Async Thread Poolโ
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("async-worker-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
See also: Java Concurrency โ Thread Pools & Executors for
ThreadPoolExecutorconstructor walkthrough, starvation math, andScheduledExecutorService.
2. Tomcat โ Embedded Server Threadsโ
What is Tomcat?โ
Apache Tomcat is the default embedded servlet container in Spring Boot. It handles HTTP connections and dispatches requests to your controllers. Tomcat uses a thread-per-request model: each incoming HTTP request gets a dedicated thread from a pool.
Tomcat's Internal Architectureโ
Internet
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ TOMCAT โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Connector (HTTP/1.1) โ โ
โ โ โ โ
โ โ Acceptor Thread โโโ OS socket backlog โ โ
โ โ โ โ โ
โ โ โผ โ โ
โ โ Poller Thread(s) โโโ NIO selector โ โ
โ โ โ (epoll/kqueue) โ โ
โ โ โผ โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ Worker Thread Pool โ โ โ
โ โ โ [T1] [T2] [T3] ... [T200] โ โ โ
โ โ โ max-threads=200 (default) โ โ โ
โ โ โโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ DispatcherServlet (Spring) โ โ
โ โ โ Controller โ โ
โ โ โ Service โ โ
โ โ โ Repository (JDBC โ HikariCP) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
How Tomcat Processes a Requestโ
1. Acceptor Thread
- Calls ServerSocketChannel.accept()
- Accepts TCP connections from the OS accept queue
- Registers the new socket with the Poller
2. Poller Thread (NIO)
- Monitors all registered sockets via Selector (epoll/kqueue)
- Waits for data to arrive on any socket
- When data is ready, hands the socket to a Worker Thread
3. Worker Thread (from the pool)
- Reads the HTTP request
- Invokes the servlet (DispatcherServlet โ your @Controller)
- Writes the HTTP response
- Returns to the pool
The worker thread is OCCUPIED for the entire request lifecycle.
If your controller makes a 2-second DB query, the thread is
blocked for 2 seconds doing nothing.
Tomcat Configurationโ
server:
tomcat:
# === Worker Thread Pool ===
threads:
max: 200 # Maximum worker threads (default: 200)
min-spare: 10 # Minimum idle threads kept warm (default: 10)
# === Connection Limits ===
max-connections: 8192 # Max simultaneous connections the Poller can track (default: 8192)
accept-count: 100 # OS-level TCP backlog queue when max-connections is reached (default: 100)
# === Timeouts ===
connection-timeout: 20000 # ms to wait for first byte after TCP connect (default: 20s)
# === Keep-Alive ===
keep-alive-timeout: 20000 # ms to keep an idle connection open for reuse
max-keep-alive-requests: 100 # requests per keep-alive connection before closing
Understanding the Numbersโ
max-connections (8192) โ Poller can track this many sockets
โ
max-threads (200) โ Only 200 can be actively processed
โ
accept-count (100) โ OS queues 100 more when max-connections hit
โ
Beyond that โ TCP RST (connection refused)
In steady state with short requests:
200 threads ร 10ms avg response = 20,000 requests/sec throughput
With slow requests (2s average):
200 threads ร 2000ms = 200 concurrent users max
User #201 waits in the Poller queue
Tomcat vs Jetty vs Undertowโ
| Feature | Tomcat | Jetty | Undertow |
|---|---|---|---|
| Default in Spring Boot | โ Yes | No | No |
| Threading model | Thread-per-request (NIO) | Thread-per-request (NIO) | XNIO (non-blocking) |
| WebSocket support | โ | โ | โ |
| HTTP/2 | โ | โ | โ |
| Memory footprint | Medium | Lower | Lowest |
| Best for | General purpose | Lightweight apps | High-performance, reactive |
See also: Spring Boot Internals โ Embedded Server Architecture for how Spring Boot auto-configures the servlet container.
3. Netty โ Event Loop Architectureโ
What is Netty?โ
Netty is an asynchronous, event-driven network application framework for building high-performance protocol servers and clients. Unlike Tomcat's thread-per-request model, Netty uses a small number of event loop threads to handle thousands of connections simultaneously.
Netty is the engine behind: Spring WebFlux, gRPC-Java, Cassandra Driver, Elasticsearch transport, Vert.x, and Kafka clients.
How Netty Works Internallyโ
Traditional (Tomcat): 1 Thread = 1 Connection
Thread-1 โ [read][process][write] โ blocked during I/O
Thread-2 โ [read][process][write] โ blocked during I/O
...
Thread-200 โ [read][process][write]
โ 200 threads = 200 concurrent connections max
Netty: 1 Thread = MANY Connections
EventLoop-1 โ [read fd1][read fd5][write fd3][read fd9][write fd1]...
EventLoop-2 โ [read fd2][read fd7][write fd4][read fd8][write fd6]...
...
EventLoop-8 โ [read fd10][write fd12][read fd15]...
โ 8 threads = 10,000+ concurrent connections
Netty Architecture (Boss-Worker Model)โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ NETTY โ
โ โ
โ Boss EventLoopGroup (1 thread typically) โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ BossEventLoop โ โ
โ โ Selector.select() โ accept new connections โ โ
โ โ Register accepted channels with Worker EventLoop โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โผ (hand off new channel) โ
โ โ
โ Worker EventLoopGroup (N threads, default = 2 ร CPU cores) โ
โ โโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโ โ
โ โ WorkerLoop-1 โ โ WorkerLoop-2 โ โ WorkerLoop-N โ โ
โ โ โ โ โ โ โ โ
โ โ Channels: โ โ Channels: โ โ Channels: โ โ
โ โ [fd1,fd5,fd9] โ โ [fd2,fd6,fd10] โ โ [fd4,fd8,fd12] โ โ
โ โ โ โ โ โ โ โ
โ โ Event Loop: โ โ Event Loop: โ โ Event Loop: โ โ
โ โ select() โ โ select() โ โ select() โ โ
โ โ โ read events โ โ โ read events โ โ โ read events โ โ
โ โ โ run pipeline โ โ โ run pipeline โ โ โ run pipeline โ โ
โ โ โ write events โ โ โ write events โ โ โ write events โ โ
โ โโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโ โ
โ โ
โ Each channel has a ChannelPipeline: โ
โ โโโโโโโโโโโโโฌโโโโโโโโโโโโฌโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโ โ
โ โ Decoder โ Encoder โ Idle โ Business Logic โ โ
โ โ (bytesโ โ (objectโ โ Handler โ Handler โ โ
โ โ object) โ bytes) โ โ โ โ
โ โโโโโโโโโโโโโดโโโโโโโโโโโโดโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Key Netty Conceptsโ
| Concept | What It Is | Analogy |
|---|---|---|
| Channel | An open connection (socket) | A phone line |
| EventLoop | A single thread running an infinite select() loop | A switchboard operator |
| EventLoopGroup | A pool of EventLoops | The operator team |
| ChannelPipeline | Chain of handlers processing data | Assembly line |
| ChannelHandler | A processing step (decode, encode, business logic) | A station on the assembly line |
| ByteBuf | Netty's buffer (replaces java.nio.ByteBuffer) | A smarter byte array with read/write indexes |
Netty Configurationโ
EventLoopGroup bossGroup = new NioEventLoopGroup(1); // 1 thread for accepting
EventLoopGroup workerGroup = new NioEventLoopGroup(); // default: 2 ร CPU cores
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
// TCP backlog โ OS-level queue for pending connections
.option(ChannelOption.SO_BACKLOG, 1024)
// Child channel options (per-connection settings)
.childOption(ChannelOption.TCP_NODELAY, true) // Disable Nagle's algorithm
.childOption(ChannelOption.SO_KEEPALIVE, true) // Enable TCP keep-alive
.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) // Pooled memory
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline()
.addLast(new HttpServerCodec()) // HTTP encode/decode
.addLast(new HttpObjectAggregator(65536)) // Aggregate HTTP chunks
.addLast(new IdleStateHandler(60, 30, 0)) // Detect idle connections
.addLast(new MyBusinessHandler()); // Your logic
}
});
NEVER block an EventLoop thread. If your handler does blocking I/O (JDBC query, synchronous HTTP call, Thread.sleep()), you block that EventLoop โ and ALL channels assigned to it are frozen.
// โ NEVER DO THIS in a ChannelHandler
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
// This blocks the EventLoop โ freezes hundreds of connections
String result = jdbcTemplate.queryForObject("SELECT ...", String.class);
ctx.writeAndFlush(result);
}
// โ
Offload blocking work to a separate thread pool
private final EventExecutorGroup blockingGroup =
new DefaultEventExecutorGroup(16); // dedicated pool for blocking ops
ch.pipeline().addLast(blockingGroup, new MyBlockingHandler());
See also: Socket Programming & I/O Models for epoll, the Reactor pattern, and how Netty uses them under the hood.
4. HikariCP โ Database Connection Poolingโ
What is HikariCP?โ
HikariCP is the fastest JVM connection pool and the default in Spring Boot 2.x+. It manages a cache of pre-opened, pre-authenticated database connections that threads borrow and return โ eliminating the 10โ100ms overhead of establishing a new connection per request.
How HikariCP Works Internallyโ
HikariCP Internals
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โ ConcurrentBag (lock-free data structure) โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Thread-local list โ try borrow from own list โ โ
โ โ โ miss โ โ
โ โ Shared list โ CAS-based steal from shared โ โ
โ โ โ miss โ โ
โ โ Handoff queue โ wait with park/unpark โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ HouseKeeper Thread (every 30s): โ
โ โโโ Evict idle connections beyond minimum-idle โ
โ โโโ Retire connections older than max-lifetime โ
โ โโโ Create new connections if below minimum-idle โ
โ โ
โ Connection validation: โ
โ โโโ On borrow: Connection.isValid(timeout) โ
โ โโโ Keepalive: periodic ping (keepalive-time) โ
โ โโโ Max-lifetime: recycle after age limit โ
โ โ
โ Metrics (Micrometer integration): โ
โ โโโ hikaricp.connections.active โ
โ โโโ hikaricp.connections.idle โ
โ โโโ hikaricp.connections.pending โ
โ โโโ hikaricp.connections.timeout โ
โ โโโ hikaricp.connections.usage (histogram) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The ConcurrentBag โ Why HikariCP Is So Fastโ
HikariCP's secret weapon is ConcurrentBag, a lock-free data structure:
Step 1: Thread-Local Borrow (fastest path โ no contention)
Thread A previously returned Connection C1
Thread A requests a connection
โ ConcurrentBag checks Thread A's thread-local list
โ C1 is there! Borrow it in ~250 nanoseconds
Step 2: Shared List Steal (fast โ CAS operation)
Thread B requests a connection (first time, no thread-local history)
โ Check shared connection list
โ CAS (compare-and-swap) to claim an idle connection
โ ~500 nanoseconds
Step 3: Handoff Queue (slowest โ waits for a return)
All connections are borrowed
Thread C requests a connection
โ Parks the thread (waits up to connection-timeout)
โ When any thread returns a connection, C is unparked
โ If timeout expires โ SQLTransientConnectionException
HikariCP Configuration (Production-Ready)โ
spring:
datasource:
url: jdbc:postgresql://db-host:5432/mydb
username: ${DB_USER}
password: ${DB_PASSWORD}
hikari:
# === Pool Size ===
maximum-pool-size: 20 # Total connections (active + idle)
minimum-idle: 20 # Fixed-size pool (no dynamic churn)
# === Timeouts ===
connection-timeout: 3000 # 3s โ fail fast, don't queue for 30s
idle-timeout: 600000 # 10min โ only matters if min-idle < max
max-lifetime: 1800000 # 30min โ recycle before DB kills them
validation-timeout: 3000 # 3s โ how long to test a connection
# === Health ===
keepalive-time: 30000 # Ping idle connections every 30s
leak-detection-threshold: 5000 # Warn if connection held > 5s
# === Identity ===
pool-name: HikariPool-Orders
Pool Sizing Formulaโ
connections = (CPU_cores ร 2) + effective_spindle_count
Where:
CPU_cores = physical cores on the DATABASE server
effective_spindle_count = 1 for SSD, disk count for RAID
Examples:
4-core DB, SSD: (4 ร 2) + 1 = 9 โ set to 10
8-core DB, SSD: (8 ร 2) + 1 = 17 โ set to 20
Divide by app instances:
20 total connections, 4 pods โ 5 per pod
Set minimum-idle = maximum-pool-size. A dynamic pool that shrinks during quiet periods means cold-start latency during the next traffic spike (new connections take 10โ100ms each).
See also: Database Connection Pooling for the complete guide on pool starvation, failure modes, PgBouncer, RDS Proxy, and anti-patterns.
5. How They All Relateโ
The Full Request Flowโ
Understanding how thread pools, Tomcat, Netty, and HikariCP interact in a single HTTP request is the key to diagnosing performance issues.
- Spring MVC (Tomcat)
- Spring WebFlux (Netty)
- Spring MVC + Virtual Threads
HTTP Request arrives
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ TOMCAT โ
โ Acceptor โ Poller โ Worker Pool โ
โ Thread "http-nio-8080-exec-42" โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โผ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ Spring DispatcherServlet โ โ
โ @Controller โ @Service โ โ
โ โ โ
โ orderService.getOrder(42) โ โ
โ โ โ โ
โ โผ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ HikariCP โ โ Tomcat worker thread is โ
โ โ Borrow connection โ โ BLOCKED during the entire โ
โ โ โ Execute SQL query โ โ DB query + response write โ
โ โ โ Return connection โ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ
โ Build response โ return โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โผ โ
Response sent back โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ
Thread "http-nio-8080-exec-42" returns to Tomcat's pool
Key insight: The Tomcat worker thread is occupied for the entire request lifecycle โ including time spent waiting for the DB.
HTTP Request arrives
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ NETTY โ
โ Boss EventLoop โ Worker EventLoop โ
โ Thread "reactor-http-nio-3" โโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โผ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ Spring WebFlux โ โ
โ RouterFunction / @Controller โ โ
โ โ โ
โ orderService.getOrder(42) โ โ
โ โ โ EventLoop thread โ
โ โผ returns Mono<Order> โ is FREE here! โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ It handles other โ
โ โ R2DBC (reactive DB driver) โ โ connections while โ
โ โ Non-blocking query โ โ waiting for DB. โ
โ โ โ DB responds asynchronously โ โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ
โ Mono completes โ write response โโโ EventLoop picks โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ this up again โ
โ โ
โผ โ
Response sent back โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ โ
Key insight: The Netty EventLoop thread is never blocked. It handles other connections while the DB query is in-flight.
HTTP Request arrives
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ TOMCAT (with Virtual Threads) โ
โ Worker = Virtual Thread โ
โ VThread "vt-http-42" โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Spring DispatcherServlet โ
โ @Controller โ @Service โ
โ โ
โ orderService.getOrder(42) โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ HikariCP โ โ Virtual thread UNMOUNTS
โ โ Borrow connection โ โ from carrier thread.
โ โ (if pool full โ VT parks) โ โ Carrier is free to run
โ โ โ Execute SQL query โ โ other virtual threads!
โ โ (VT unmounts during I/O) โ โ
โ โ โ Return connection โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Build response โ return โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
Response sent back
Virtual thread is garbage collected (not returned to a pool)
Key insight: Virtual threads give you Tomcat's simple programming model (blocking code) with Netty-like efficiency (threads aren't wasted during I/O).
The Relationship Diagramโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ YOUR APPLICATION โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ HTTP Server โ โ Database Access โ โ
โ โ โ โ โ โ
โ โ โโโโโ Tomcat โโโโโโโ โ โโโโ HikariCP โโโโโโโโโโโโ โ
โ โ โ Thread Pool โโ โ โ Connection Pool โโ โ
โ โ โ (200 workers) โโ โ โ (20 connections) โโ โ
โ โ โโโโโโโโโโโโโโโโโโโโ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ โโโโโ Netty โโโโโโโโ โ โ โ
โ โ โ EventLoopGroup โโ โ โโโโ R2DBC Pool โโโโโโโโโโโ โ
โ โ โ (8 event loops) โโ โ โ Reactive connections โโ โ
โ โ โโโโโโโโโโโโโโโโโโโโ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ Application Thread Pools โโ
โ โ โโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโ โโ
โ โ โ @Async pool โ โ @Scheduled pool โ โโ
โ โ โ (business logic)โ โ (cron/timer tasks) โ โโ
โ โ โโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโ โโ
โ โ โโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโ โโ
โ โ โ ForkJoinPool โ โ Virtual Thread Executor โ โโ
โ โ โ (parallelStream)โ โ (Java 21+) โ โโ
โ โ โโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโ โโ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Concurrency Model Comparisonโ
| Aspect | Tomcat (BIO/NIO) | Netty | Virtual Threads |
|---|---|---|---|
| Model | Thread-per-request | Event loop | Virtual-thread-per-request |
| Threads needed | ~200 for 200 concurrent | ~8 for 10,000+ concurrent | Millions possible |
| Blocking I/O | Blocks a worker thread | โ Must never block | โ Safe โ unmounts from carrier |
| Code style | Simple imperative | Callback/reactive | Simple imperative |
| Memory per connection | ~1MB (thread stack) | ~1KB (channel state) | ~1KB (heap continuation) |
| Spring integration | Spring MVC | Spring WebFlux | Spring MVC (3.2+) |
| DB access | JDBC + HikariCP | R2DBC (reactive) | JDBC + HikariCP |
| Best for | Traditional CRUD APIs | High-connection servers | I/O-heavy APIs on Java 21+ |
6. Production Sizing Guideโ
The Bottleneck Chainโ
Internet โ Load Balancer โ Tomcat Threads โ HikariCP Connections โ Database CPU
โ โ โ
Bottleneck 1 Bottleneck 2 Bottleneck 3
If Tomcat has 200 threads but HikariCP has 10 connections:
โ 190 threads will queue waiting for a connection
โ If those threads also serve other endpoints, the ENTIRE API stalls
If HikariCP has 200 connections but the DB only has 8 CPU cores:
โ 192 queries queue in the DB waiting for CPU
โ Query latency spikes โ connection hold time increases โ pool exhaustion
Sizing Checklistโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Component โ Formula / Rule โ
โโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Tomcat max-threads โ Start at 200 (default). Tune down if CPU โ
โ โ context-switching dominates. โ
โโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ HikariCP pool-size โ (DB_CPU ร 2) + 1, divided by # app pods โ
โ โ Example: 8-core DB, 4 pods โ 5 per pod โ
โโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Netty workers โ 2 ร CPU cores (default). Rarely needs โ
โ โ adjustment. โ
โโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ @Async pool โ CPU-bound: cores + 1 โ
โ โ I/O-bound: cores ร (1 + wait/compute) โ
โโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ ForkJoinPool โ Defaults to CPU cores. Only for CPU-bound โ
โ โ parallel work (parallel streams). โ
โโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Virtual Threads โ Don't pool them. Unlimited. Use Semaphore โ
โ โ to guard downstream resources. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Mismatch Deadlockโ
A critical failure mode when thread pool and connection pool sizes don't match:
Scenario:
Tomcat max-threads = 200
HikariCP maximum-pool-size = 10
connection-timeout = 30s (default)
Step 1: 200 requests arrive simultaneously
Step 2: Threads 1โ10 borrow all 10 connections
Step 3: Threads 11โ200 queue for connections (up to 30s each)
Step 4: Thread 1 (holding C1) makes an internal REST call to /api/helper
Step 5: /api/helper request arrives โ needs a connection too
Step 6: All connections are held โ /api/helper waits 30s
Step 7: Thread 1 waits for /api/helper โ can't release C1
Step 8: DEADLOCK โ nobody makes progress
Fix:
1. Set connection-timeout to 3s (fail fast)
2. Use separate pools for internal sub-requests
3. Ensure pool size โฅ max threads that need concurrent connections
7. Troubleshooting & Common Failuresโ
Symptoms โ Diagnosis โ Fixโ
| Symptom | Likely Cause | Diagnosis | Fix |
|---|---|---|---|
| Response times spike under load | Pool starvation (thread or connection) | Check hikaricp.connections.pending > 0 or high thread count | Reduce connection-timeout, fix slow queries |
SQLTransientConnectionException | All connections borrowed, timeout expired | hikaricp.connections.timeout counter increasing | Increase pool size OR fix connection hold time |
RejectedExecutionException | Thread pool + queue both full | Thread dump shows all threads busy | Increase queue or threads; fix slow handlers |
| CPU at 100% with no useful work | Too many threads โ context switching | vmstat shows high cs (context switch) rate | Reduce thread count |
| Memory growing (OOM) | Unbounded queue or thread count | Heap dump shows many task objects or threads | Use bounded queues; explicit ThreadPoolExecutor |
| Tomcat stops accepting requests | All 200 worker threads blocked | Thread dump shows all threads in WAITING on HikariCP | Fix connection leak; reduce connection-timeout |
| Netty EventLoop blocked | Blocking call in a ChannelHandler | Slow channel handlers, increasing event loop latency | Offload blocking work to separate executor |
Essential Metrics to Monitorโ
# Spring Boot Actuator + Micrometer
# Tomcat thread pool
tomcat.threads.current # Current thread count
tomcat.threads.busy # Threads actively processing requests
tomcat.threads.config.max # Maximum configured threads
# HikariCP connection pool
hikaricp.connections.active # Connections currently in use
hikaricp.connections.idle # Connections sitting idle
hikaricp.connections.pending # Threads waiting for a connection โ ALERT if > 0
hikaricp.connections.timeout # Connection borrow timeouts (cumulative)
hikaricp.connections.usage # Connection hold time histogram
# JVM threads
jvm.threads.live # Total live threads
jvm.threads.peak # Peak thread count since startup
jvm.threads.daemon # Daemon threads
Thread Dump Analysisโ
# Get a thread dump of your Java process
jstack <pid> > threaddump.txt
# What to look for:
# 1. Many threads in WAITING state on HikariCP
"http-nio-8080-exec-42" WAITING
at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:162)
โ Pool starvation โ all connections are borrowed
# 2. Many threads BLOCKED on synchronized
"http-nio-8080-exec-15" BLOCKED
at com.example.LegacyService.criticalSection(LegacyService.java:42)
โ Lock contention โ single synchronized method is a bottleneck
# 3. Deadlock detected
"Found one Java-level deadlock:"
โ Thread A holds Lock 1, waits for Lock 2
โ Thread B holds Lock 2, waits for Lock 1
8. Interview Questionsโ
Q: Explain the relationship between Tomcat's thread pool and HikariCP's connection pool.โ
A: Tomcat's thread pool handles HTTP requests โ each request gets a worker thread. When that request needs the database, the worker thread borrows a connection from HikariCP. The Tomcat thread is blocked until the DB query completes and the connection is returned. If HikariCP has fewer connections than Tomcat has threads (common: 200 threads vs 10โ20 connections), excess threads queue. This is fine for fast queries (under 5ms) but dangerous for slow queries โ threads pile up waiting, leading to pool starvation and cascading timeouts.
Q: Why does Netty need far fewer threads than Tomcat?โ
A: Tomcat uses thread-per-request: each thread blocks during I/O. With 200 threads, you handle 200 concurrent requests max. Netty uses the Reactor pattern: a few EventLoop threads multiplex thousands of connections via epoll/kqueue. When data isn't ready on a socket, the EventLoop serves another channel instead of blocking. This means 8 threads can handle 10,000+ concurrent connections โ but you must never block an EventLoop thread, or all its channels freeze.
Q: What happens when you enable virtual threads in Spring Boot 3.2+?โ
A: Setting spring.threads.virtual.enabled=true makes Tomcat use virtual threads instead of platform threads for request handling. Each request gets its own virtual thread (not from a fixed pool). When the virtual thread blocks on I/O (JDBC query, HTTP call), it unmounts from the carrier thread โ the carrier is free to run other virtual threads. This gives Tomcat-like simplicity (blocking code) with Netty-like efficiency (threads aren't wasted during I/O). The new bottleneck shifts from threads to connection pools โ you must size HikariCP and use Semaphores to prevent 100K virtual threads from overwhelming the database.
Q: How would you size a HikariCP pool for a 4-pod deployment against an 8-core RDS instance?โ
A: Use the formula: connections = (CPU_cores ร 2) + 1 = 17. Round to 20 total connections. With 4 pods: 20 / 4 = 5 connections per pod. Set maximum-pool-size: 5 and minimum-idle: 5 (fixed pool). If you scale to 8 pods without adjusting, you'd get 40 total connections โ overloading the DB. Either reduce per-pod pool size or add PgBouncer/RDS Proxy as a connection multiplexer.
Q: Why is Executors.newFixedThreadPool() considered dangerous?โ
A: It uses an unbounded LinkedBlockingQueue. If tasks arrive faster than threads can process them, the queue grows without limit โ consuming heap memory until OutOfMemoryError. In production, always use ThreadPoolExecutor directly with a bounded ArrayBlockingQueue and a rejection policy like CallerRunsPolicy for backpressure.
Q: How do you diagnose pool starvation?โ
A: Monitor hikaricp.connections.pending (threads waiting for connections) and hikaricp.connections.timeout (failed borrows). Take a thread dump โ if many threads are in WAITING state at HikariPool.getConnection(), the pool is starved. Root causes: slow queries (N+1, missing indexes), connections held during non-DB work (@Transactional wrapping HTTP calls), or pool too small for the workload. Fix the query first; increase pool size only as a last resort.
๐ Cross-Referencesโ
| Topic | Link |
|---|---|
| ThreadPoolExecutor & Fork/Join details | Java Concurrency |
| Virtual Threads deep dive | Virtual Threads (Project Loom) |
| HikariCP anti-patterns & PgBouncer | Database Connection Pooling |
| Netty, epoll, and the Reactor pattern | Socket Programming & I/O Models |
| Tomcat embedded server internals | Spring Boot Internals |
| JVM memory & thread stacks | JVM Memory Architecture |
| Spring Boot server tuning | Spring Boot Advanced |