Backend Development 19 min read

Understanding Java ThreadFactory, Rejection Policies, and Dynamic Thread Pool Management

This article explains Java's ThreadFactory interface, demonstrates how to customize thread names, explores the four built‑in rejection policies of ThreadPoolExecutor with code examples, and shows techniques for dynamic adjustment of core and maximum pool sizes, including custom policies and blocking queue task submission.

FunTester
FunTester
FunTester
Understanding Java ThreadFactory, Rejection Policies, and Dynamic Thread Pool Management

Java's ThreadFactory interface in the java.util.concurrent package allows developers to customize thread creation, such as setting thread names, priorities, and groups. The default factory generates names like pool-1-thread-1 via Executors.defaultThreadFactory() .

The default factory's constructor initializes a name prefix using a static counter, and its newThread(Runnable r) method creates a non‑daemon thread with normal priority.

Example of a custom thread factory that names threads FunTester-1 , FunTester-2 , etc.:

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

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class ThreadFactoryDemo {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(0, 5, 60L, TimeUnit.SECONDS,
                new SynchronousQueue<>(), new ThreadFactory() {
            AtomicInteger index = new AtomicInteger(1);
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r);
                thread.setName("FunTester-" + index.getAndIncrement());
                return thread;
            }
        });
        for (int i = 0; i < 3; i++) {
            executor.execute(() -> {
                try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); }
                System.out.println(Thread.currentThread().getName() + " is running");
            });
        }
        executor.shutdown();
    }
}

Running this code prints thread names like FunTester-2 is running , improving readability.

The four built‑in rejection policies of ThreadPoolExecutor are demonstrated:

AbortPolicy – throws RejectedExecutionException when the pool is full.

DiscardPolicy – silently discards the rejected task.

DiscardOldestPolicy – removes the oldest queued task and retries execution.

CallerRunsPolicy – runs the rejected task in the calling thread.

Each policy is shown with a small program, its console output, and an explanation of the observed behavior.

A custom rejection handler that logs the rejection, waits one second, and then retries the task is also provided:

ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(1), new RejectedExecutionHandler() {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        System.out.println(Thread.currentThread().getName() + "  " + System.currentTimeMillis() + "  Task rejected, pool full");
        try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); }
        executor.execute(r);
    }
});

Another technique uses a blocking queue directly to submit tasks, which blocks when the queue is full:

ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 2, 10L, TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(2));
executor.prestartCoreThread();
for (int i = 0; i < 4; i++) {
    int seq = i;
    Runnable r = () -> {
        try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); }
        System.out.println(Thread.currentThread().getName() + "  " + System.currentTimeMillis() + "  Task" + seq + " completed");
    };
    executor.getQueue().put(r); // blocks if queue is full
    System.out.println(Thread.currentThread().getName() + "  " + System.currentTimeMillis() + "  Task" + seq + " submitted");
}
executor.shutdown();

Dynamic adjustment of core and maximum pool sizes is illustrated using ThreadPoolExecutor#setCorePoolSize and #setMaximumPoolSize . A sample class DynamicThreadPool shows how to increase or decrease these values based on the current queue size, providing a simple but functional auto‑scaling mechanism.

public class DynamicThreadPool {
    public static ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 2, 10L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(100));
    public static void execute(Runnable command) {
        dynamic();
        executor.execute(command);
    }
    public static void dynamic() {
        int size = executor.getQueue().size();
        if (size > 0) {
            increaseCorePoolSize();
            if (size > 100) {
                increaseMaximumPoolSize();
            } else {
                decreaseMaximumPoolSize();
            }
        } else {
            decreaseCorePoolSize();
        }
    }
    // methods increaseCorePoolSize, decreaseCorePoolSize, increaseMaximumPoolSize, decreaseMaximumPoolSize omitted for brevity
}

The article also notes important constraints: setting a maximum size smaller than the core size throws IllegalArgumentException , while setting a core size larger than the maximum does not error but may create more active threads than the declared maximum.

In summary, the chapter covers Java concurrency fundamentals, detailed usage of ThreadFactory , rejection policies, blocking task submission, and dynamic thread‑pool tuning, providing practical code snippets for each concept.

JavaThreadPoolExecutorRejectionPolicyThreadFactoryDynamicThreadPool
FunTester
Written by

FunTester

10k followers, 1k articles | completely useless

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.