Fundamentals 12 min read

Why Using Executors to Build Thread Pools Can Hurt Your Java Apps—and How to Do It Right

This article explains the pitfalls of creating Java thread pools with Executors, details how to correctly build them using ThreadPoolExecutor, clarifies core parameters, queue strategies, and rejection policies, and provides runnable code examples to illustrate proper usage.

Programmer DD
Programmer DD
Programmer DD
Why Using Executors to Build Thread Pools Can Hurt Your Java Apps—and How to Do It Right

Drawbacks of Creating Thread Pools with Executors

Most developers use Executors to create thread pools, but this practice violates Alibaba's coding guidelines and can lead to resource exhaustion.

Issues include:

newFixedThreadPool and newSingleThreadExecutor : the unbounded request queue may consume massive memory and cause OOM.

newCachedThreadPool and newScheduledThreadPool : they can create up to Integer.MAX_VALUE threads, also risking OOM.

Creating Thread Pools with ThreadPoolExecutor

Replace the non‑standard code with a direct ThreadPoolExecutor construction:

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), handler);
}
ThreadPoolExecutor

is the core implementation of a thread pool, reducing thread creation overhead and providing built‑in statistics for monitoring.

ThreadPoolExecutor Parameter Explanation

corePoolSize & maximumPoolSize

When a task is submitted:

If the running thread count is less than corePoolSize, a new thread is created.

If the count is between corePoolSize and maximumPoolSize, a new thread is created only when the queue is full.

If the count exceeds maximumPoolSize, the task is handled by the rejection policy.

keepAliveTime & unit

Specifies the maximum idle time for threads that exceed corePoolSize, with unit defining the time unit.

Waiting Queue

Any BlockingQueue can be used, and its size interacts with the pool size:

If running threads < corePoolSize, a new thread is created for each task.

If running threads ≥ corePoolSize, tasks are queued; when the queue is full and threads < maximumPoolSize, new threads are created.

If threads > maximumPoolSize, the rejection policy is applied.

Common Queue Strategies

Direct handoff (SynchronousQueue): tasks are handed directly to a thread; if none is available, a new thread is created.

Unbounded queue (LinkedBlockingQueue): tasks wait in an unbounded queue, preventing creation of threads beyond corePoolSize.

Bounded queue (ArrayBlockingQueue): limits queue size, helping avoid resource exhaustion but making tuning more complex.

Rejection Policies

When the pool is shut down or saturated, four built‑in policies are available:

AbortPolicy : throws RejectedExecutionException.

CallerRunsPolicy : runs the task in the calling thread.

DiscardPolicy : silently discards the task.

DiscardOldestPolicy : discards the oldest queued task and retries the new one.

Custom policies can be implemented by providing a RejectedExecutionHandler.

ThreadPoolExecutor Creation Demo

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolSerialTest {
    public static void main(String[] args) {
        int corePoolSize = 3;
        int maximumPoolSize = 6;
        long keepAliveTime = 2;
        TimeUnit unit = TimeUnit.SECONDS;
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<Runnable>(2);
        ThreadPoolExecutor threadPoolExecutor = null;
        try {
            threadPoolExecutor = new ThreadPoolExecutor(corePoolSize,
                maximumPoolSize,
                keepAliveTime,
                unit,
                workQueue,
                new ThreadPoolExecutor.AbortPolicy());
            for (int i = 0; i < 8; i++) {
                final int index = i + 1;
                threadPoolExecutor.submit(() -> {
                    System.out.println("Hello, I am thread: " + index);
                    try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); }
                });
                Thread.sleep(500);
            }
        } catch (InterruptedException e) { e.printStackTrace(); }
        finally { threadPoolExecutor.shutdown(); }
    }
}

The execution flow:

ThreadPoolExecutor is instantiated.

Eight tasks are submitted (corePoolSize = 3, queue capacity = 2, maximumPoolSize = 6).

First three tasks create core threads; the next two are queued; the sixth task creates an extra thread; the seventh and eighth create threads up to the maximum.

When core threads finish, they pick queued tasks, avoiding frequent thread creation and destruction.

Demonstrating Rejection Policies

Submitting nine tasks (exceeding maximumPoolSize + queue capacity) triggers the policies. The following images illustrate each policy's behavior:

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.

Javathread poolExecutorServiceThreadPoolExecutor
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

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.