Backend Development 10 min read

Designing a Flexible Java ThreadPoolExecutor: From Basics to Advanced Features

This article walks through building a custom thread pool executor in Java, starting with a simple thread creation example, evolving through multiple design iterations, and culminating in a full-featured implementation that supports core and maximum pool sizes, task queues, thread factories, and rejection policies.

macrozheng
macrozheng
macrozheng
Designing a Flexible Java ThreadPoolExecutor: From Basics to Advanced Features

Xiao Yu asks Flash to explain thread pools, feeling confused by the many parameters.

Flash starts with the simplest asynchronous execution:

<code>new Thread(r).start();</code>

He then defines a basic

Executor

interface:

<code>public interface Executor {
    void execute(Runnable r);
}</code>

Flash implements a first version of a tool class:

<code>class FlashExecutor implements Executor {
    public void execute(Runnable r) {
        new Thread(r).start();
    }
}</code>

Realizing the need to control thread creation, they design a worker thread that pulls tasks from a queue, ensuring only a limited number of threads run:

<code>class FlashExecutor implements Executor {
    // corePoolSize, workQueue, etc.
    public void execute(Runnable r) {
        tasks.offer(r);
    }
    // Worker threads continuously take tasks from the queue
}</code>

Flash highlights three major benefits of this design: controlling thread count, decoupling task submission from execution via a queue, and reducing overhead from repeatedly creating and destroying threads.

Further improvements include:

Creating workers on demand instead of all at once, tracking created workers with

workCount

.

Adding a

RejectedExecutionHandler

to let callers decide how to handle rejected tasks.

Introducing a

ThreadFactory

so new threads are created through a user‑provided factory.

They illustrate the enhanced design with diagrams (images omitted for brevity).

The final version mirrors the real

ThreadPoolExecutor

constructor:

<code>public FlashExecutor(
    int corePoolSize,
    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue&lt;Runnable&gt; workQueue,
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler) {
    // parameter validation omitted
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}</code>

The parameters mean:

corePoolSize : number of core threads that stay alive.

maximumPoolSize : maximum number of threads allowed.

keepAliveTime : idle time before non‑core threads are terminated.

unit : time unit for

keepAliveTime

.

workQueue : thread‑safe blocking queue holding pending tasks.

threadFactory : factory for creating new threads.

handler : policy for handling rejected tasks.

This comprehensive design ensures the thread pool can handle normal loads with core threads, scale up during spikes with additional threads up to

maximumPoolSize

, and shrink back by terminating idle non‑core threads, while giving callers control over task rejection and thread creation.

JavaConcurrencymultithreadingExecutorThreadPoolExecutor
macrozheng
Written by

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.

0 followers
Reader feedback

How this landed with the community

login 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.