Why You Should Avoid Executors and Build ThreadPoolExecutor Manually in Java
This article explains the definition of thread pools, why using Executors to create them is discouraged, details the ThreadPoolExecutor constructor and its parameters, compares different Executors factory methods, demonstrates OOM risks with tests, and provides practical guidelines for configuring safe thread pools.
Thread Pool Definition
A thread pool manages a group of worker threads, offering benefits such as reduced resource creation, lower system overhead, and improved stability by preventing unlimited thread creation that can cause OutOfMemoryError (OOM).
Executors Creation Methods
Executors can create thread pools in three ways, all returning a ThreadPoolExecutor object:
newCachedThreadPool – creates a cached thread pool
newSingleThreadExecutor – creates a single‑thread pool
newFixedThreadPool – creates a fixed‑size pool
ThreadPoolExecutor Object
The ThreadPoolExecutor constructor has the following 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 for keepAliveTime
workQueue – task queue used by the pool
threadFactory – factory for creating new threads
handler – policy for handling rejected tasks
Task Execution Logic and Parameter Relationship
The execution flow is:
If core threads are not full, a new core thread is created to run the task.
If core threads are full, the task is offered to the workQueue; if the queue is not full, it is enqueued.
If the queue is full, and the pool has not reached maximumPoolSize, a new non‑core thread is created.
If the pool is also full, the handler policy is applied to reject the task.
Executors Returning ThreadPoolExecutor
Only the methods that return a ThreadPoolExecutor are discussed.
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 = 60 seconds
workQueue = SynchronousQueue (no buffering)
When a task is submitted, no core thread exists, the SynchronousQueue is always “full”, so a new non‑core thread is created. Idle non‑core threads are reclaimed after 60 s, but 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 = 0
workQueue = LinkedBlockingQueue (unbounded)
Only one core thread is created; additional tasks are queued in an unbounded queue, which can lead to OOM and renders maximumPoolSize and keepAliveTime ineffective.
newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>&()); }Characteristics:
corePoolSize = nThreads
maximumPoolSize = nThreads
keepAliveTime = 0
workQueue = LinkedBlockingQueue (unbounded)
Similar to SingleThreadExecutor but with a configurable core size; the unbounded queue can also cause OOM.
Summary of OOM Risks
FixedThreadPool and SingleThreadExecutor use an unbounded queue (Integer.MAX_VALUE), which may accumulate many tasks and cause OOM.
CachedThreadPool can create an unlimited number of threads (Integer.MAX_VALUE), also leading to OOM.
Therefore, using Executors static factories is discouraged; creating a ThreadPoolExecutor directly is recommended.
OOM Exception Test
Test class TaskTest.java creates an unbounded cached thread pool and continuously submits tasks, triggering OOM when the JVM heap is limited (e.g., -Xms10M -Xmx10M). Sample output shows an OutOfMemoryError after creating tens of thousands of threads.
public class TaskTest { public static void main(String[] args) { ExecutorService es = Executors.newCachedThreadPool(); int i = 0; while (true) { es.submit(new Task(i++)); } } }How to Define Thread Pool Parameters
CPU‑bound workloads : pool size ≈ CPU count + 1 (obtainable via Runtime.availableProcessors()).
IO‑bound workloads : pool size ≈ CPU count × CPU utilization × (1 + waitTime / cpuTime).
Mixed workloads : separate CPU‑bound and IO‑bound tasks into different pools.
Blocking queue : prefer a bounded queue to avoid resource exhaustion.
Rejection policy : the default AbortPolicy throws RejectedExecutionException. More graceful options include:
Catch RejectedExecutionException and handle the task manually.
Use CallerRunsPolicy to run the task in the calling thread.
Implement a custom RejectedExecutionHandler.
For low‑importance tasks, DiscardPolicy or DiscardOldestPolicy can drop tasks.
When using Executors static methods, applying a Semaphore for rate‑limiting can also prevent OOM.
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.
21CTO
21CTO (21CTO.com) offers developers community, training, and services, making it your go‑to learning and service platform.
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.
