Why Tomcat’s Thread Pool Works Differently from the JDK’s
This article explains how Tomcat customizes Java’s ThreadPoolExecutor for I/O‑intensive web workloads, detailing the key parameters, the custom task queue and thread factory, the overridden execute logic, and the role of the submittedCount variable in preventing thread starvation under high concurrency.
Web containers such as Tomcat improve throughput by delegating request handling to a thread pool. The standard JDK ThreadPoolExecutor is tuned for CPU‑bound tasks and therefore does not suit the typical I/O‑heavy workload of a servlet container, prompting Tomcat to implement its own modifications.
Key ThreadPoolExecutor Parameters
Tomcat focuses on two essential limits:
Maximum number of threads (core and max threads)
Maximum length of the work queue
Both limits must be enforced, otherwise a high‑concurrency scenario could exhaust CPU and memory.
Tomcat’s Custom Thread Pool Construction
// Custom task queue
TaskQueue taskqueue = new TaskQueue(maxQueueSize);
// Custom thread factory
TaskThreadFactory tf = new TaskThreadFactory(namePrefix, daemon, getThreadPriority());
// Custom thread pool
executor = new ThreadPoolExecutor(
getMinSpareThreads(), // core threads (minSpareThreads)
getMaxThreads(), // max threads (maxThreads)
maxIdleTime,
TimeUnit.MILLISECONDS,
taskqueue,
tf);The two configurable thread counts are:
Core threads – minSpareThreads Maximum threads –
maxThreadsTomcat’s Overridden execute Logic
Tomcat rewrites the execute method to add a four‑step handling process:
When the number of active threads is below corePoolSize, a new thread is created for each incoming task.
Once the core pool is full, tasks are placed into the queue; idle threads pull from the queue.
If the queue is full but the total thread count is still below maximumPoolSize, Tomcat attempts to create a temporary thread.
If both the queue and the thread pool are saturated, the task is rejected according to the configured rejection policy.
The main difference from the plain JDK implementation is step 3: Tomcat does not immediately reject a task when the pool reaches its maximum size; it first tries to enqueue the task, and only if that fails does it apply the rejection policy.
Implementation Details
The overridden method catches RejectedExecutionException from the JDK pool and, if the underlying queue is a TaskQueue, it calls force to attempt insertion with a timeout. If insertion still fails, the task is rejected.
public void execute(Runnable command, long timeout, TimeUnit unit) {
submittedCount.incrementAndGet();
try {
// Delegate to JDK executor
super.execute(command);
} catch (RejectedExecutionException rx) {
// When max threads are reached, JDK would reject immediately
if (super.getQueue() instanceof TaskQueue) {
TaskQueue queue = (TaskQueue) super.getQueue();
try {
// Try to enqueue with timeout
if (!queue.force(command, timeout, unit)) {
submittedCount.decrementAndGet();
throw new RejectedExecutionException("...");
}
} finally {
// (optional cleanup)
}
}
}
}The TaskQueue extends LinkedBlockingQueue and receives a capacity value from Tomcat’s maxQueueSize parameter. By default maxQueueSize is Integer.MAX_VALUE, which would cause the queue to always accept tasks and prevent the creation of new threads.
Custom offer Logic
To avoid the “unbounded queue” problem, TaskQueue overrides offer so that it returns false at the right moment, signalling the executor to spawn a new thread.
public boolean offer(Runnable o) {
// If pool already at max, just queue the task
if (parent.getPoolSize() == parent.getMaximumPoolSize())
return super.offer(o);
// max > current pool size > core
// 1. If submitted tasks <= current pool size, there are idle threads
if (parent.getSubmittedCount() <= parent.getPoolSize())
return super.offer(o);
// 2. If submitted tasks > current pool size, we need more threads
if (parent.getPoolSize() < parent.getMaximumPoolSize())
return false; // trigger thread creation
// Default: queue the task
return super.offer(o);
}Tomcat also maintains a submittedCount counter that tracks tasks that have been handed to the pool but not yet completed. This counter is incremented at the start of execute and decremented when a task fails to be queued or is rejected, allowing the executor to make informed decisions about thread creation when the queue is effectively unbounded.
In summary, Tomcat’s thread pool adds a custom task queue, a bespoke thread factory, and an overridden execute method that leverages a submitted‑task counter to ensure new threads can still be created under high load, even when the queue size is set to a very large value.
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.
JavaEdge
First‑line development experience at multiple leading tech firms; now a software architect at a Shanghai state‑owned enterprise and founder of Programming Yanxuan. Nearly 300k followers online; expertise in distributed system design, AIGC application development, and quantitative finance investing.
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.
