Why You Should Avoid Executors for Thread Pools and Use ThreadPoolExecutor Directly
This article explains the definition of Java thread pools, why creating them via Executors is discouraged, details the ThreadPoolExecutor constructor and parameters, compares different Executors factory methods, demonstrates OOM risks with tests, and offers guidelines for configuring thread pool size and rejection policies.
Preface
First, thank you for reading this article between your other activities. By the end you will understand:
What a thread pool is
Various ways Executors create thread pools
The ThreadPoolExecutor class
How task execution logic relates to thread‑pool parameters
Why Executors return a ThreadPoolExecutor object
OOM exception testing
How to define thread‑pool parameters
Thread Pool Definition
A thread pool manages a group of worker threads. Reusing threads brings several benefits:
Reduces resource creation → lowers memory overhead
Decreases system overhead → thread creation takes time and delays request handling
Improves stability → prevents unlimited thread creation that leads to OutOfMemoryError (OOM)
Ways Executors Creates Thread Pools
Based on the returned object type, thread pools can be created in three categories:
Return a ThreadPoolExecutor object
Return a ScheduledThreadPoolExecutor object
Return a ForkJoinPool object
This article focuses on the first case – creating a ThreadPoolExecutor object.
ThreadPoolExecutor Object
Before discussing the static factory methods in Executors, we introduce ThreadPoolExecutor. The class has four constructors, all ultimately delegating to the same signature:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)Parameter meanings: corePoolSize → number of core threads maximumPoolSize → maximum number of threads keepAliveTime → idle thread keep‑alive time unit → time unit workQueue → task queue used by the pool threadFactory → factory that creates new threads handler → policy for handling rejected tasks
Thread‑Pool Task Execution Logic and Parameter Relationship
The execution flow works as follows:
If the number of core threads is not full (controlled by corePoolSize), a new core thread is created to run the task.
If core threads are full, the queue is checked (controlled by workQueue). If the queue is not full, the task is enqueued.
If the queue is full, the pool checks whether it can create additional threads (controlled by maximumPoolSize). If possible, a non‑core thread is created.
If the pool is also full, the rejection policy (controlled by handler) is applied.
Executors Methods That Return ThreadPoolExecutor
There are three static methods in Executors that return a ThreadPoolExecutor: Executors#newCachedThreadPool → creates a cached thread pool Executors#newSingleThreadExecutor → creates a single‑thread pool Executors#newFixedThreadPool → creates a fixed‑size pool
newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}Characteristics:
corePoolSize = 0 maximumPoolSize = Integer.MAX_VALUE(practically unlimited)
keepAliveTime = 60L seconds workQueue = SynchronousQueue(no buffering)
When a task is submitted, no core thread is created; the task is handed off to a newly created non‑core thread because the queue is always considered full. Idle non‑core threads are reclaimed after 60 seconds. The unbounded thread count can easily cause OOM.
newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService(
new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}Characteristics:
corePoolSize = 1 maximumPoolSize = 1 keepAliveTime = 0L workQueue = LinkedBlockingQueue(unbounded)
Only one core thread exists; additional tasks are queued. Because the queue is effectively infinite, it can also lead to OOM under heavy load, and the maximum pool size and keep‑alive settings become irrelevant.
newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}Characteristics:
corePoolSize = nThreads maximumPoolSize = nThreads keepAliveTime = 0L workQueue = LinkedBlockingQueue(unbounded)
Similar to SingleThreadExecutor but with a configurable core size; the unbounded queue still poses OOM risk.
Summary
FixedThreadPool and SingleThreadExecutor use an unbounded queue (capacity = Integer.MAX_VALUE), which can accumulate many tasks and cause OOM.
CachedThreadPool can create an unlimited number of threads, also leading to OOM.
Therefore, using Executors to create thread pools is discouraged; creating a ThreadPoolExecutor directly gives full control over parameters and avoids these pitfalls.
OOM Exception Test
To verify the theory, a simple test class is used:
public class TaskTest {
public static void main(String[] args) {
ExecutorService es = Executors.newCachedThreadPool();
int i = 0;
while (true) {
es.submit(new Task(i++));
}
}
}Run the test with a small JVM heap (e.g., -Xms10M -Xmx10M). The program quickly throws an OutOfMemoryError after creating tens of thousands of threads.
How to Define Thread‑Pool Parameters
CPU‑bound tasks: pool size ≈ number of CPUs + 1 (obtainable via Runtime.availableProcessors()).
I/O‑bound tasks: pool size ≈ CPU count × CPU utilization × (1 + waitTime / CPUTime).
Mixed workloads: separate CPU‑bound and I/O‑bound tasks into different pools.
Work queue: prefer a bounded queue to prevent resource exhaustion.
Rejection policy: the default AbortPolicy throws RejectedExecutionException, which is often undesirable. Recommended alternatives include:
Catch the exception and handle the task manually. CallerRunsPolicy – the submitting thread runs the task, throttling further submissions.
Implement a custom RejectedExecutionHandler.
Use DiscardPolicy or DiscardOldestPolicy for low‑priority tasks.
When using Executors static methods, you can also apply a Semaphore to limit concurrency and avoid OOM.
Feel free to share additional experience or improvements.
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.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
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.
