Mastering Custom ThreadPoolExecutor: Build Flexible Java Thread Pools

This article explains why the standard Executors factory methods may be insufficient, details each ThreadPoolExecutor constructor parameter, demonstrates how to configure a custom pool for performance testing, and walks through multiple code examples that illustrate thread creation, queue behavior, and rejection policies.

FunTester
FunTester
FunTester
Mastering Custom ThreadPoolExecutor: Build Flexible Java Thread Pools

The standard java.util.concurrent.Executors factory methods are convenient but lack flexibility for complex multithreading scenarios, prompting the need for custom thread pools that allow precise control over thread count, lifecycle, and scalability.

corePoolSize

Defines the number of core threads that are always kept alive. New tasks create threads until this number is reached; thereafter tasks are queued. Core threads are retained even when idle beyond the keep‑alive time.

maximumPoolSize

Specifies the maximum number of threads the pool can grow to. Threads beyond the core count are created only when the work queue is full.

keepAliveTime and unit

Together they set the maximum idle time for non‑core threads. When a thread remains idle longer than this duration, it is terminated to free resources.

workQueue

The queue that holds tasks awaiting execution. It must be a BlockingQueue<Runnable> and provides thread‑safe storage.

Thread‑safe queue implementation.

Producers block when the queue is full.

Only objects of type java.lang.Runnable are accepted.

threadFactory

Creates new Thread instances for the pool. Implementations must provide Thread newThread(Runnable r) to wrap a task in a thread.

handler

Defines the rejection policy invoked when a task cannot be submitted. The JDK supplies four standard policies: AbortPolicy: throws RejectedExecutionException. DiscardPolicy: silently discards the task. DiscardOldestPolicy: discards the oldest queued task and retries. CallerRunsPolicy: runs the task in the calling thread.

Key practical insights:

New threads are created either when a task arrives and the current thread count is below corePoolSize, or when the queue is full and the count is below maximumPoolSize.

Rejection policies are triggered only after both the queue is full and the pool has reached its maximum size.

Choosing an appropriate queue capacity is critical: an overly large queue can cause tasks to pile up and effectively reduce the pool to a fixed‑size executor.

Selecting a suitable rejection strategy balances throughput and resource usage; custom policies can be implemented when the built‑in ones are insufficient.

Example 1 – Core 0, Max 2, Queue capacity 2

package org.funtester.performance.books.chapter01.section3;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * Thread pool creation demo
 */
public class CreateThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(0, 2, 60, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(2)); // create pool
        for (int i = 0; i < 4; i++) {
            int index = i; // task identifier
            Thread.sleep(200);
            executor.execute(() -> {
                try {
                    Thread.sleep(1000); // simulate work
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(Thread.currentThread().getName() + "  " + System.currentTimeMillis() + "  " + index + "  execute task");
            });
            System.out.println(Thread.currentThread().getName() + "  " + System.currentTimeMillis() + "  " + index + "  submit task");
        }
        executor.shutdown(); // no new tasks, finish queued ones
    }
}

Running this code produces four submission logs followed by a single execution thread because the pool initially creates only one thread (core size 0, max 2, but the queue is not full).

main  1712996368590  0  submit task
main  1712996368795  1  submit task
main  1712996369000  2  submit task
main  1712996369201  3  submit task
pool-1-thread-1  1712996369591  0  execute task
pool-1-thread-1  1712996370592  1  execute task
pool-1-thread-1  1712996371593  2  execute task
pool-1-thread-1  1712996372594  3  execute task

Example 2 – Queue capacity 2

main  1712996440747  0  submit task
main  1712996440950  1  submit task
main  1712996441155  2  submit task
main  1712996441359  3  submit task
pool-1-thread-1  1712996441748  0  execute task
pool-1-thread-2  1712996442361  3  execute task
pool-1-thread-1  1712996442749  1  execute task
pool-1-thread-2  1712996443362  2  execute task

Analysis shows that after the queue fills, the pool creates a second thread to handle the fourth task, while earlier tasks wait in the queue.

Example 3 – Core 2, Max 3, Queue 10

main  1712997072632  0  submit task
main  1712997072832  1  submit task
main  1712997073038  2  submit task
main  1712997073242  3  submit task
pool-1-thread-1  1712997073643  0  execute task
pool-1-thread-2  1712997073834  1  execute task
pool-1-thread-1  1712997074644  2  execute task
pool-1-thread-2  1712997074834  3  execute task

Here the pool immediately creates two core threads because the current count is below corePoolSize, even though the queue is not full.

The underlying execution logic can be seen in ThreadPoolExecutor#execute:

if (isRunning(c) && workQueue.offer(command)) {
    int recheck = ctl.get();
    if (!isRunning(recheck) && remove(command))
        reject(command);
    else if (workerCountOf(recheck) == 0)
        addWorker(null, false);
} else if (!addWorker(command, false)) {
    reject(command);
}

When the queue cannot accept a task, addWorker is invoked with the task and false indicating a non‑core thread.

JavaconcurrencyPerformance TestingThreadPoolJava concurrencyThreadPoolExecutorcustom-thread-pool
FunTester
Written by

FunTester

10k followers, 1k articles | completely useless

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.