Technical Specification Snapshot
| Parameter | Description |
|---|---|
| Language | Java |
| License | Original article marked as CC 4.0 BY-SA |
| Stars | Not provided in the source |
| Core Dependencies | ThreadPoolExecutor, BlockingQueue, AQS, ExecutorService |
The essence of thread reuse in a thread pool is a continuous task-fetching execution loop
Many developers assume that thread pool “reuse” means a thread finishes execution, gets recycled, and is then reassigned to a new task. That understanding is not accurate. Inside a thread pool, a thread usually does not exit immediately after completing a task. Instead, it remains alive and waits for the next task.
The real key is the Worker execution model inside ThreadPoolExecutor: once a thread starts, it enters runWorker(), executes its initial task, and then continuously fetches subsequent tasks from the blocking queue. As long as it can still retrieve tasks, the thread keeps working.
A single while loop is the core condition behind thread reuse
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask; // Execute the initial task bound when the Worker was created
w.firstTask = null;
w.unlock(); // Allow interrupt control only after unlocking
try {
while (task != null || (task = getTask()) != null) { // Core reuse loop
w.lock(); // Lock during task execution to avoid accidental interruption during shutdown
try {
task.run(); // Execute the user-submitted task
} finally {
task = null; // Task is complete; prepare to fetch the next one from the queue
w.completedTasks++;
w.unlock();
}
}
} finally {
processWorkerExit(w, false); // The thread exits only after breaking out of the loop
}
}
This code shows that thread reuse does not mean calling start() again. Instead, the same thread executes multiple Runnable instances continuously inside a loop.
Worker is not a simple thread wrapper but a combination of thread and state control
Worker is an internal class of ThreadPoolExecutor, and its design is extremely elegant. It both implements Runnable and extends AQS, so it can serve not only as the thread execution body but also as the holder of runtime state and interrupt control.
Inside Worker, two key objects matter: thread represents the actual worker thread that gets started, and firstTask represents the initial task bound immediately when the Worker is created. This lets the thread pool expand by creating a thread and starting it with a task already attached.
The structure of Worker lets it both run tasks and manage interrupts
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
final Thread thread; // The actual thread associated with this Worker
Runnable firstTask; // The first task to execute
volatile long completedTasks; // Number of completed tasks
Worker(Runnable firstTask) {
setState(-1); // Initialize to a non-interruptible state
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this); // Create the thread using this Worker as the Runnable
}
@Override
public void run() {
runWorker(this); // Enter the unified work loop after the thread starts
}
}
This structure shows that the real unit of reuse in a thread pool is not the Thread object alone, but the task execution unit centered around Worker.
getTask determines whether a thread keeps waiting or exits after a timeout
If runWorker() is responsible for execution, then getTask() is responsible for keeping the worker alive. It determines whether a Worker can obtain the next task after finishing one, or whether it should end its lifecycle.
This is where core and non-core threads diverge. Core threads usually wait indefinitely by using take(). Non-core threads usually wait with a timeout via poll(keepAliveTime), which may return null after a timeout and trigger thread exit.
The differentiated waiting strategy in getTask defines the elasticity of the thread pool
private Runnable getTask() {
boolean timedOut = false;
for (;;) {
boolean timed = allowCoreThreadTimeOut || workerCountOf(ctl.get()) > corePoolSize;
try {
Runnable r = timed
? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) // Timed wait for non-core threads
: workQueue.take(); // Indefinite blocking wait for core threads
if (r != null) {
return r; // Continue reusing the current thread after obtaining a task
}
timedOut = true; // No task was obtained before timeout; the thread may exit later
} catch (InterruptedException retry) {
timedOut = false; // Retry task retrieval after interruption
}
}
}
The role of this code is to choose the waiting strategy based on thread type and decide whether the thread should stay alive when no tasks are available.
From task submission to thread reuse, the system forms a complete execution path
After a thread pool receives a task, it first checks whether the current thread count is smaller than the core pool size. If it is, the pool creates a Worker directly and places the task into firstTask. If the core threads are already full, the task goes into workQueue first. If the queue is also full, the pool then tries to create a non-core thread.
As a result, a task typically lands in one of three places: it is handed directly to a newly created core thread, it enters the queue and waits for an idle thread to pick it up, or it is assigned to a newly created non-core thread. No matter which path it takes, it eventually converges into the unified execution loop inside runWorker().
The submission flow can be reduced to a thread pool scheduling framework
ExecutorService pool = new ThreadPoolExecutor(
4, // Core pool size
8, // Maximum pool size
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100) // Tasks enter the blocking queue first
);
pool.submit(() -> {
// Business task logic executed by a Worker thread
System.out.println(Thread.currentThread().getName());
});
The meaning of this code is simple: task submission happens only once, while thread reuse is completed automatically by the internal loop and queueing mechanism of the thread pool.
Thread pools have clear advantages over manually creating threads, especially under high concurrency
If every task uses new Thread().start(), thread creation, destruction, and scheduling context switches all introduce significant overhead. In workloads with many short-lived tasks, system performance can be consumed by thread management costs alone.
A thread pool, by contrast, uses a limited number of Worker instances to process a large volume of tasks continuously. It decouples the number of threads from the number of tasks. A larger number of tasks does not imply that the number of threads must grow proportionally. That is the fundamental reason thread pools improve throughput and stability.
A side-by-side code comparison makes the cost difference of reuse obvious
Runnable task = () -> {
// Simulate a business task
System.out.println("do work");
};
for (int i = 0; i < 1000; i++) {
new Thread(task).start(); // Creates a new thread every time; extremely expensive
}
ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
pool.submit(task); // 10 threads process 1000 tasks in a loop
}
This code demonstrates the essential difference between the two models: the former reuses only task logic, while the latter reuses thread execution capacity.
In interviews, you should clearly explain that a thread pool is not a borrow-and-return model but a loop-based execution model
The easiest way to lose points in an interview is to describe thread reuse as an object-pool-style borrow-and-return mechanism. The accurate explanation is this: a thread starts once, remains resident for a long time, and repeatedly calls run() on different tasks inside a while loop.
Additional points that strengthen your answer include explaining why Worker extends AQS, understanding the value of the beforeExecute and afterExecute hooks, and knowing the different waiting strategies between core and non-core threads.
FAQ
FAQ 1: Why does a thread not exit after finishing one task?
Because the thread associated with the Worker has not finished its run() method. It remains inside the while loop of runWorker() and calls getTask() to fetch the next task. As long as the queue can still provide tasks, the thread stays alive.
FAQ 2: What is the difference between core threads and non-core threads in thread reuse?
Core threads wait indefinitely for tasks through take() by default, so they are more stable. Non-core threads wait with a timeout through poll(keepAliveTime), and they may exit after being idle for too long. Both can be reused, but their keep-alive strategies differ.
FAQ 3: Why does Worker extend AQS?
Because the thread pool needs to distinguish whether a thread is idle or busy. Worker uses AQS to implement lightweight mutual exclusion, which allows shutdown() to interrupt only idle threads and avoids accidentally interrupting threads that are actively executing tasks.
Key Takeaway
This article focuses on how Java thread pools implement thread reuse by breaking down the collaboration between Worker, runWorker(), and getTask() inside ThreadPoolExecutor. It explains the key differences among firstTask, the blocking queue, core threads, and timeout-based recycling of non-core threads.