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.

21CTO
21CTO
21CTO
Why You Should Avoid Executors and Build ThreadPoolExecutor Manually in Java

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaThreadPoolOOMThreadPoolExecutorExecutors
21CTO
Written by

21CTO

21CTO (21CTO.com) offers developers community, training, and services, making it your go‑to learning and service platform.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.