Master Java Thread Pools: Boost Performance and Avoid Resource Pitfalls
This article explains why creating a thread for each task is inefficient, introduces thread pools as a solution, compares execution times with code examples, details ThreadPoolExecutor's core interfaces, constructors, execution flow, rejection policies, state transitions, and provides practical usage patterns and best‑practice recommendations for Java backend development.
01 Background Introduction
Although Java provides extensive support for thread creation, interruption, waiting, notification, destruction, and synchronization, creating and destroying threads frequently consumes significant time and resources from the operating system perspective.
When many tasks need to be processed simultaneously, a naive model creates one thread per task, which works for a few tasks but quickly leads to problems as the number of tasks grows:
Thread count becomes uncontrollable, making unified management impossible.
System overhead becomes huge; excessive thread creation can exhaust resources and even cause the system to crash.
Using a pool of threads to execute many tasks—known as a thread pool —solves these issues.
02 Thread Pool Overview
A thread pool maintains a set of reusable threads. Idle threads wait for tasks; when a new task arrives, an idle thread is assigned. If all threads are busy, the task is queued, a new thread may be created, or the task may be rejected.
The advantages are clear:
Resources become controllable, preventing resource exhaustion.
Resource consumption is lower because threads are reused, reducing creation and destruction overhead.
Execution efficiency improves, as tasks can start immediately without waiting for thread creation.
For a practical open‑source example, the mall project is a SpringBoot3 + Vue e‑commerce system (GitHub ★60K) that demonstrates these concepts.
Below is a simple comparison:
<code>/**
* One task per thread
*/
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
final Random random = new Random();
List<Integer> list = new CopyOnWriteArrayList<>();
// 20,000 threads, each adding a random number
for (int i = 0; i < 20000; i++) {
new Thread(() -> list.add(random.nextInt(100))).start();
}
while (list.size() < 20000) {}
System.out.println("One task per thread time: " + (System.currentTimeMillis() - startTime) + "ms");
}
/**
* Using a thread pool (4 threads)
*/
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
final Random random = new Random();
List<Integer> list = new CopyOnWriteArrayList<>();
ThreadPoolExecutor executor = new ThreadPoolExecutor(4, 4, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(20000));
for (int i = 0; i < 20000; i++) {
executor.submit(() -> list.add(random.nextInt(100)));
}
while (list.size() < 20000) {}
System.out.println("Thread pool time: " + (System.currentTimeMillis() - startTime) + "ms");
executor.shutdown();
}
</code>Results:
<code>One task per thread time: 3073ms
---------------------------
Thread pool time: 578ms
</code>The dramatic speedup demonstrates why thread pools are essential.
02.1 Thread Pool Core Interfaces
In Java, the top‑level interface is
Executor. Key implementations include:
Executor: abstracts task execution from thread creation.
ExecutorService: extends
Executor</b> with lifecycle management methods such as shutdown and task status queries.</li><li><code>ThreadPoolExecutor: the core class that actually creates and manages the pool.
ScheduledThreadPoolExecutor: supports timed and periodic task execution.
2.1 ThreadPoolExecutor Constructor
The constructor takes seven parameters; the most important are corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, and handler. Example core source:
<code>public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) { ... }
</code>Parameter meanings:
corePoolSize: number of core threads that stay alive.
maximumPoolSize: maximum allowed threads.
keepAliveTime: idle thread survival time (effective only when thread count exceeds corePoolSize).
unit: time unit for keepAliveTime.
workQueue: queue that holds pending tasks.
threadFactory: creates new threads.
handler: strategy for rejected tasks.
2.2 Execution Flow
When a task is submitted, the pool follows these steps:
If the current thread count is less than corePoolSize, a new thread is created immediately.
Otherwise, the task is offered to the workQueue.
If the queue is full, the pool tries to create a new thread (up to maximumPoolSize).
If both the queue and thread count are saturated, the configured
RejectedExecutionHandleris invoked.
Key methods:
execute(Runnable): core task submission method without a return value.
submit(Runnable/Callable): submits a task and returns a
Futurefor result retrieval; internally it still calls
execute.
2.3 Rejection Policies
AbortPolicy: throws
RejectedExecutionException(default).
DiscardPolicy: silently discards the task.
DiscardOldestPolicy: removes the oldest queued task and retries.
CallerRunsPolicy: runs the task in the calling thread if the pool is shut down.
2.4 ThreadPoolExecutor States
The pool has five states:
RUNNING: accepts new tasks and processes queued ones.
SHUTDOWN: stops accepting new tasks but processes queued tasks.
STOP: stops all processing and discards queued tasks.
TIDYING: all tasks finished, awaiting termination.
TERMINATED: final state after termination.
Transition examples:
Calling
shutdown()moves from RUNNING to SHUTDOWN.
Calling
shutdownNow()moves to STOP.
When both thread count and queue are empty, the pool moves to TIDYING, then to TERMINATED after
terminated()completes.
03 Thread Pool Applications
Typical usage steps:
<code>// 1. Create a fixed‑size pool (4 threads, 15‑second keep‑alive, queue capacity 1000)
ThreadPoolExecutor executor = new ThreadPoolExecutor(
4, 4, 15, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
// 2. Submit tasks
executor.submit(task1);
executor.submit(task2);
// ...
// 3. Shut down when done
executor.shutdown();
</code>Both
execute()and
submit()can be used;
submit()additionally returns a
Futurefor result retrieval.
Factory Methods Summary
Executors.newSingleThreadExecutor(): core=1, max=1, unbounded
LinkedBlockingQueue.
Executors.newFixedThreadPool(n): core=n, max=n, unbounded
LinkedBlockingQueue.
Executors.newCachedThreadPool(): core=0, max=Integer.MAX_VALUE, 60‑second keep‑alive,
SynchronousQueue(creates threads on demand).
Executors.newScheduledThreadPool(core): core as specified, max=Integer.MAX_VALUE, uses
DelayedWorkQueuefor timed tasks.
Because the default factories use unbounded queues or unlimited thread counts, they can cause OOM in high‑concurrency scenarios. It is recommended to create pools directly via
ThreadPoolExecutorwith explicit limits.
Custom Thread Factories
Giving threads meaningful names helps debugging. Example using Guava’s
ThreadFactoryBuilder:
<code>ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setNameFormat("order-%d")
.setDaemon(true)
.build();
</code>Or implement your own:
<code>public final class NamingThreadFactory implements ThreadFactory {
private final AtomicInteger threadNum = new AtomicInteger();
private final ThreadFactory delegate;
private final String name;
public NamingThreadFactory(ThreadFactory delegate, String name) {
this.delegate = delegate;
this.name = name;
}
@Override
public Thread newThread(Runnable r) {
Thread t = delegate.newThread(r);
t.setName(name + "-" + threadNum.incrementAndGet());
return t;
}
}
</code>Thread Count Guidelines
CPU‑bound tasks: set thread count to N (CPU cores) + 1.
I/O‑bound tasks: set thread count to 2N.
Determine the type by checking whether the task performs network/file I/O (I/O‑bound) or pure computation (CPU‑bound).
macrozheng
Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.
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.