Junior 5 min · March 09, 2026

Java Thread Pools - Unbounded Queue Exhausts Heap

Unbounded queue millions Runnable -> GC every 2s, HTTP 503.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Java Executor Service decouples task submission from thread management, reusing a pool of worker threads.
  • Key components: ThreadPoolExecutor, task queue, saturation policy, and custom thread factories.
  • Performance insight: Thread creation costs ~1ms per thread; reusing 4 threads can handle 1000s of tasks/second.
  • Production insight: An unbounded queue silently consumes all heap memory, causing OutOfMemoryError with no obvious stack trace.
  • Biggest mistake: Assuming newFixedThreadPool is safe without monitoring queue depth and rejection rates.
Plain-English First

Think of Java Executor Service and Thread Pools as a powerful tool in your developer toolkit. Once you understand what it does and when to reach for it, everything clicks into place. Imagine a busy restaurant kitchen. Without a pool, every time an order (task) comes in, you have to hire and train a new chef, who then leaves as soon as the dish is done—a massive waste of time and energy. With a Thread Pool, you have a fixed team of chefs (threads) waiting. When an order arrives, it goes into a queue, and the next available chef grabs it. This keeps the kitchen efficient and prevents you from hiring 500 chefs at once and crashing your budget (memory).

Java Executor Service and Thread Pools is a fundamental concept in Java development. Introduced in Java 5 as part of the java.util.concurrent package, it decoupled task submission from the mechanics of how each task is run. Before this, developers were forced to manually manage the lifecycle of every thread, leading to 'Thread Leakage' and unmanageable resource spikes.

In this guide, we'll break down exactly what Java Executor Service and Thread Pools is, why it was designed to replace the manual 'new Thread().start()' approach, and how to use it correctly in real projects to build scalable systems. We will examine how a properly tuned pool can mean the difference between a responsive microservice and a crashed JVM.

By the end, you'll have both the conceptual understanding and practical code examples to use Java Executor Service and Thread Pools with confidence.

If you're running this in a containerised environment, thread pools interact with CPU limits in surprising ways — a 4-core container doesn't automatically mean 4 active threads. We'll cover that too.

What Is Java Executor Service and Thread Pools and Why Does It Exist?

Java Executor Service and Thread Pools is a core feature of Concurrency. It was designed to solve a specific problem that developers encounter frequently: the high overhead of thread creation and destruction. Creating a thread is a 'heavyweight' operation involving the OS kernel, memory allocation for the stack, and initialization logic.

By reusing existing threads to execute multiple tasks, the Executor Service reduces latency and prevents resource exhaustion. It provides a managed environment to control the number of concurrent threads, handle task queuing, and manage the lifecycle of background workers. At io.thecodeforge, we consider this the 'Gold Standard' for managing asynchronous workloads because it provides a centralized place to monitor and throttle application pressure.

Think about it: every time you use new Thread(r).start(), you're burning CPU and memory for a one-shot task. The OS has to allocate a new stack (~1MB), set up thread-local storage, and schedule it. Doing this for 10,000 requests will kill your server. The Executor Service reuses threads, so the overhead is paid once per thread, not per task.

io/thecodeforge/concurrency/ForgeTaskProcessor.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package io.thecodeforge.concurrency;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * io.thecodeforge production example: Managed Task Execution
 */
public class ForgeTaskProcessor {
    public void processBatch() {
        // io.thecodeforge: Using a fixed pool to prevent thread explosion
        ExecutorService executor = Executors.newFixedThreadPool(4);

        for (int i = 0; i < 10; i++) {
            final int taskId = i;
            executor.submit(() -> {
                String threadName = Thread.currentThread().getName();
                System.out.println("Forge Worker [" + threadName + "] processing task: " + taskId);
                try { 
                    TimeUnit.MILLISECONDS.sleep(500); 
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }

        // Graceful shutdown is non-negotiable for production code
        executor.shutdown();
        try {
            if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                executor.shutdownNow();
            }
        } catch (InterruptedException e) {
            executor.shutdownNow();
        }
    }
}
Output
Forge Worker [pool-1-thread-1] processing task: 0
Forge Worker [pool-1-thread-2] processing task: 1
...
Key Insight:
The most important thing to understand about Java Executor Service and Thread Pools is the problem it was designed to solve. Always ask 'why does this exist?' before asking 'how do I use it?' In this case, it exists to provide efficient resource management and task scheduling.
Production Insight
Using Executors.newFixedThreadPool without thinking about queue capacity is the #1 production mistake.
The default LinkedBlockingQueue is unbounded — it will grow until the JVM runs out of heap.
Rule: always construct ThreadPoolExecutor directly with a bounded queue.
Key Takeaway
Executor Service exists to amortise thread creation cost.
Use it for any async task, but never with unbounded queues.
Bounded queues + rejection policies = resilient systems.

Common Mistakes and How to Avoid Them

When learning Java Executor Service and Thread Pools, most developers hit the same set of gotchas. Knowing these in advance saves hours of debugging. A common mistake is using 'unbounded' queues (like Executors.newCachedThreadPool()) for high-load I/O tasks. Under heavy load, a cached pool will keep creating new threads until the system runs out of memory.

Another frequent error is forgetting to shut down the executor, which keeps the JVM running even after the main work is finished. To avoid this, always manage the lifecycle of your executors. In production environments at io.thecodeforge, we avoid the Executors factory for critical paths and instead use ThreadPoolExecutor directly to gain granular control over the queue capacity and 'Saturation Policies'—deciding what happens when the pool is full.

A third mistake that catches teams off-guard is not handling exceptions from submitted tasks. If a task throws a runtime exception, the thread is removed and a new one is created — you lose the exception unless you wrap the task with a try-catch or use a Future.get() to surface it.

io/thecodeforge/concurrency/config/SafeExecutorConfig.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package io.thecodeforge.concurrency.config;

import java.util.concurrent.*;

public class SafeExecutorConfig {
    /**
     * io.thecodeforge: Best practice - Manual configuration for production.
     * Prevents OutOfMemoryErrors by bounding the task queue.
     */
    public static ExecutorService createProductionPool() {
        return new ThreadPoolExecutor(
            10,                      // corePoolSize: Threads kept alive
            25,                      // maximumPoolSize: Max threads allowed
            60L, TimeUnit.SECONDS,   // keepAliveTime: Idle thread expiration
            new LinkedBlockingQueue<>(500), // Bounded queue: Capacity limit
            new ThreadPoolExecutor.CallerRunsPolicy() // Saturation policy: Throttles producer
        );
    }
}
Output
// Returns a robust, bounded executor service ready for production traffic.
Watch Out:
The most common mistake with Java Executor Service and Thread Pools is using it when a simpler alternative would work better. Always consider whether the added complexity is justified, such as using Parallel Streams for simple data-processing tasks where the overhead of an Executor Service might be unnecessary.
Production Insight
if you use execute() instead of submit(), unchecked exceptions silently kill the worker thread.
The pool creates a new thread, but you never see the error.
Rule: always use submit() and handle the Future, or wrap tasks with a custom afterExecute in ThreadPoolExecutor.
Key Takeaway
Never use Executors factory methods in production.
Always provide a bounded queue and a explicit saturation policy.
Wrap all submitted tasks with exception handling.

ThreadPoolExecutor Internals: Core, Max, Queue, and Saturation Policy

To use thread pools correctly, you need to understand the execution flow inside ThreadPoolExecutor. When a task is submitted:

  1. If the current number of running threads is less than corePoolSize, a new thread is created to handle the task (even if idle threads exist).
  2. If running threads >= corePoolSize, the task is placed in the work queue.
  3. If the queue is full and running threads < maximumPoolSize, a new thread is created.
  4. If the queue is full and running threads == maximumPoolSize, the rejection (saturation) policy is triggered.

The order matters: core -> queue -> max -> reject. Many engineers incorrectly assume that maximumPoolSize threads are created first, then the queue is used. Actually, the queue is used after core, and max threads only kick in when the queue fills. That's why a bounded queue is essential — without it, max threads are never reached and the system appears healthy until the queue overflows memory.

io/thecodeforge/concurrency/config/CustomThreadPoolExecutor.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package io.thecodeforge.concurrency.config;

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

/**
 * Custom ThreadPoolExecutor with monitoring hooks.
 */
public class CustomThreadPoolExecutor extends ThreadPoolExecutor {
    private final AtomicInteger submittedTasks = new AtomicInteger();
    private final AtomicInteger completedTasks = new AtomicInteger();

    public CustomThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
                                     long keepAliveTime, TimeUnit unit,
                                     BlockingQueue<Runnable> workQueue,
                                     RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
    }

    @Override
    public void execute(Runnable command) {
        submittedTasks.incrementAndGet();
        super.execute(command);
    }

    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        completedTasks.incrementAndGet();
        // Log unreported exceptions
        if (t != null) {
            System.err.println("Uncaught exception in task: " + t.getMessage());
        }
        super.afterExecute(r, t);
    }

    public int getSubmittedCount() { return submittedTasks.get(); }
    public int getCompletedCount() { return completedTasks.get(); }
}
Output
// Custom pool tracks submitted and completed tasks, logs exceptions.
Execution Order Mental Model
  • corePoolSize = always-on workers (like minimum staff)
  • Queue = holding area when staff are busy
  • maximumPoolSize = extra staff you call in when waiting room is full
  • RejectedExecutionHandler = what happens when even extra staff can't keep up
Production Insight
In a container with CPU limits, corePoolSize should be set based on the container's available processors, not the host.
Runtime.availableProcessors() returns host cores, which can oversubscribe the container.
Rule: use -XX:ActiveProcessorCount or pass container CPU limit via environment variable.
Key Takeaway
core -> queue -> max -> reject: that's the execution order.
Bounded queues enable the max pool size to be reached.
Without a bounded queue, your saturated pool is a ticking time bomb.
Choosing the Right Saturation Policy
IfYou want to throttle the producer (e.g., HTTP request thread)
UseUse CallerRunsPolicy — the calling thread executes the task, applying backpressure.
IfYou want to silently drop tasks and log a warning
UseUse a custom policy that logs and discards, but only if you have downstream monitoring.
IfYou want to abort immediately with an exception
UseUse AbortPolicy (default) — throws RejectedExecutionException, but caller must handle it.
IfYou want to discard the oldest task to make room for new ones
UseUse DiscardOldestPolicy — useful for caching or scraping where freshness matters.

Custom ThreadFactory and Naming Conventions

When you look at a thread dump from a production incident, threads named 'pool-1-thread-1' tell you nothing. Each thread pool should have a meaningful name that identifies its purpose. The ThreadFactory interface gives you control over thread creation, including naming, daemon status, priority, and uncaught exception handlers.

Without custom names, you can't tell which pool is leaking, stuck, or over-consuming CPU. A well-named thread like 'order-processor-0' immediately points you to the responsible service. This is a cheap investment that pays off every time you debug.

io/thecodeforge/concurrency/config/NamedThreadFactory.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package io.thecodeforge.concurrency.config;

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

public class NamedThreadFactory implements ThreadFactory {
    private final String namePrefix;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final boolean daemon;

    public NamedThreadFactory(String poolName, boolean daemon) {
        this.namePrefix = poolName + "-";
        this.daemon = daemon;
    }

    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r, namePrefix + threadNumber.getAndIncrement());
        t.setDaemon(daemon);
        t.setUncaughtExceptionHandler((thread, throwable) -> 
            System.err.println("Uncaught in " + thread.getName() + ": " + throwable.getMessage()));
        return t;
    }
}

// Usage:
// ExecutorService pool = new ThreadPoolExecutor(..., new NamedThreadFactory("order-processing", false));
Output
// Threads appear as 'order-processing-1', 'order-processing-2' in thread dumps.
Pro Tip
Set daemon to true for pools used for background tasks that should not prevent JVM shutdown. Set daemon to false for pools handling critical business tasks that need to complete before exit.
Production Insight
In a production incident response, every second counts. Named threads reduce the time to identify the culprit pool from minutes to seconds.
Rule: never create a thread pool without a custom ThreadFactory that includes the service name.
Key Takeaway
Thread names are your first diagnostic tool.
Always use a NamedThreadFactory.
Include service name and purpose in the prefix.

Shutdown Patterns: Graceful vs Abrupt

Shutting down a thread pool is not optional — it's a production requirement. The JVM will not exit if there are non-daemon threads still alive. Two methods: shutdown() (graceful) and shutdownNow() (abrupt).

  • shutdown() initiates an orderly shutdown: no new tasks are accepted, previously submitted tasks continue to run, and idle threads are interrupted via the interrupt() mechanism.
  • shutdownNow() attempts to stop all actively executing tasks by interrupting them, and returns a list of tasks that were waiting in the queue.

For data integrity, prefer shutdown() followed by awaitTermination() with a timeout. If the timeout expires, you can decide to call shutdownNow() to force termination. This pattern ensures that in-flight tasks get a chance to complete and commit their work.

io/thecodeforge/concurrency/config/GracefulShutdownWrapper.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package io.thecodeforge.concurrency.config;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;

public class GracefulShutdownWrapper {
    private static final Logger LOG = Logger.getLogger(GracefulShutdownWrapper.class.getName());

    public static void shutdownAndAwaitTermination(ExecutorService pool, long timeout, TimeUnit unit) {
        pool.shutdown(); // Disable new tasks
        try {
            // Wait a while for existing tasks to terminate
            if (!pool.awaitTermination(timeout, unit)) {
                LOG.warning("Pool did not terminate within timeout. Forcing shutdown.");
                pool.shutdownNow(); // Cancel currently executing tasks
                // Wait again for tasks to respond to interruption
                if (!pool.awaitTermination(30, TimeUnit.SECONDS)) {
                    LOG.severe("Pool did not terminate even after force shutdown.");
                }
            }
        } catch (InterruptedException ie) {
            // (Re-)Cancel if current thread also interrupted
            pool.shutdownNow();
            // Preserve interrupt status
            Thread.currentThread().interrupt();
        }
    }
}
Output
// Usage in Spring Boot @PreDestroy: shutdownAndAwaitTermination(executor, 60, TimeUnit.SECONDS);
Watch Out:
Calling shutdown() inside a task submitted to the same pool results in deadlock or IllegalStateException. The task will never complete if it is waiting for the pool to shutdown, and the pool waits for the task. Always shutdown from outside the pool's tasks.
Production Insight
If you use shutdownNow(), tasks that are I/O-bound may not respond to interruption if they don't check the interrupt flag.
Rule: design your tasks to respond to interruption (check Thread.currentThread().isInterrupted() periodically).
For long-running tasks, consider using a custom isTerminating flag.
Key Takeaway
Always use shutdown() + awaitTermination() with a timeout.
Don't shutdown from within pool tasks.
Design tasks to respect interruption for clean teardown.

Monitoring and Tuning Thread Pools in Production

A thread pool without monitoring is a blind spot. You need to track at least: active thread count, queue size, completed task count, rejected task count, and thread creation/destruction rate. Spring Boot Actuator exposes these via Micrometer under 'jvm.threadpool.*' metrics. In plain Java, you can expose them through JMX or a custom health check.

Tuning involves three knobs: core pool size, max pool size, and queue capacity. There's no one-size-fits-all formula, but a good starting point for CPU-bound tasks is number of CPU cores + 1. For I/O-bound tasks, you can go higher — the formula corePoolSize = number of cores * (1 + wait time / compute time) is a useful approximation.

Performance insight: If you over-provision the pool, context switching eats throughput. If you under-provision, the queue grows and tasks wait longer. Measure and adjust based on actual latency and throughput goals.

io/thecodeforge/concurrency/config/ThreadPoolMonitor.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package io.thecodeforge.concurrency.monitoring;

import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicLong;

public class ThreadPoolMonitor {
    private final ThreadPoolExecutor executor;
    private final AtomicLong lastRejectedCount = new AtomicLong(0);

    public ThreadPoolMonitor(ThreadPoolExecutor executor) {
        this.executor = executor;
    }

    public void logMetrics() {
        long active = executor.getActiveCount();
        long poolSize = executor.getPoolSize();
        long queueSize = executor.getQueue().size();
        long completed = executor.getCompletedTaskCount();
        long rejected = executor.getRejectedExecutionHandler().getClass().getSimpleName().contains("Abort") ? 
                         ? 0 : 0; // In real code, track via custom handler
        System.out.printf("Pool: %s, Active: %d, Size: %d, Queue: %d, Completed: %d%n",
            executor.toString(), active, poolSize, queueSize, completed);
    }

    public boolean isHealthy(long queueThreshold) {
        return executor.getQueue().size() < queueThreshold && 
               executor.getActiveCount() < executor.getMaximumPoolSize();
    }
}
Output
// Metrics output: Pool: java.util.concurrent.ThreadPoolExecutor@1a2b3c4, Active: 3, Size: 5, Queue: 45, Completed: 1200
Tuning Strategy
Set corePoolSize to your baseline concurrency demand, maxPoolSize to handle spikes, and queue capacity to buffer short bursts. Use a rejection policy to prevent overload. Monitor and adjust based on actual queue depth and rejection counts.
Production Insight
In a Spring Boot application, ThreadPoolTaskExecutor is often used. It wraps ThreadPoolExecutor and supports task decoration, which is useful for tracing and logging.
Rule: always enable async metrics in Spring Boot via management.metrics.export.xxx or Micrometer's default binders.
Key Takeaway
Without metrics, you're guessing.
Monitor active threads, queue depth, and rejections.
Tune based on actual wait/compute ratio, not rules of thumb.
When to Scale Pool Size
IfQueue grows steadily, active threads < max
UseIncrease corePoolSize gradually by 25% and observe latency.
IfActive threads hit max, queue grows
UseIncrease maxPoolSize or queue capacity. Consider scaling out horizontally.
IfRejections occur frequently
UseIncrease pool size OR implement backpressure. Don't just increase queue without limit.
IfThreads idle most of the time
UseReduce corePoolSize to avoid wasting resources. allowCoreThreadTimeOut(true) can shrink fully.
● Production incidentPOST-MORTEMseverity: high

Unbounded Queue Exhausts Heap in Payment Processing

Symptom
The service became unresponsive, returning HTTP 503. JVM heap dump showed millions of Runnable objects in a LinkedBlockingQueue. GC logs showed full GC every few seconds.
Assumption
The team assumed the default Executors.newFixedThreadPool(10) was safe because they controlled the number of concurrent tasks through the thread count.
Root cause
The queue was unbounded (LinkedBlockingQueue with default capacity Integer.MAX_VALUE). When downstream slowed, tasks queued up faster than workers could process them. The queue grew until heap was exhausted.
Fix
Replace defaults with explicit ThreadPoolExecutor using a bounded queue and a rejection policy. Set queue capacity to 1000 and use CallerRunsPolicy to throttle the producer.
Key lesson
  • Never use unbounded queues for production workloads — they turn a capacity issue into a memory leak.
  • Always monitor queue depth, rejection rate, and thread pool metrics via Micrometer or JMX.
  • Apply backpressure: bounded queues + caller run policy prevents the pool from being a death star.
Production debug guideDiagnose thread pool misconfiguration, task leaks, and contention fast.4 entries
Symptom · 01
Application stuck, no tasks progressing
Fix
Take thread dump: jstack <pid>. Look for threads in BLOCKED or WAITING on pool locks. Check if pool is shutdown prematurely.
Symptom · 02
Tasks rejected (RejectedExecutionException)
Fix
Check queue depth via JMX or logs. If queue is full, either increase queue capacity, scale out, or implement backpressure.
Symptom · 03
High CPU but low throughput
Fix
Check if tasks are running on more threads than CPU cores. Use pool size = Runtime.getRuntime().availableProcessors() for CPU-bound tasks.
Symptom · 04
Memory grows over time without obvious leak
Fix
Enable thread pool metrics (Micrometer) to track queue size. Dump heap: jmap -histo:live <pid> | head -20. Look for Runnables from the pool.
★ Quick Debug Commands for Thread PoolsRun these commands when you suspect thread pool trouble. No theory, just action.
What are the threads doing?
Immediate action
Take thread dump and look for pool threads.
Commands
jstack <pid> > threads.txt && grep -A5 'pool-' threads.txt
jcmd <pid> Thread.print
Fix now
If threads are BLOCKED, check locks; if WAITING, check pool shutdown status.
Is the queue backing up?+
Immediate action
Enable JMX for ThreadPoolExecutor and query queue size.
Commands
jconsole or jmc to connect to MBean: java.util.concurrent:type=ThreadPoolExecutor,*
curl localhost:8080/actuator/metrics/jvm.threadpool.queue.size
Fix now
If queue size > 80% capacity, increase pool size or apply backpressure.
Tasks being rejected?+
Immediate action
Check rejection count metric.
Commands
jstat -gcutil <pid> 1000 10 (check GC overhead)
jmap -histo <pid> | grep -i "Runnable\|Task"
Fix now
Reduce submission rate or increase queue/thread pool. Consider CallerRunsPolicy.
AspectManual Threads (new Thread())Executor Service (Thread Pool)
Resource ReuseNone (Thread dies after task completion)High (Worker threads are recycled for new tasks)
ThroughputLow (Limited by creation overhead)High (Optimized for rapid task processing)
Task QueuingNone (Task must run immediately)Built-in (Wait in queue if threads are busy)
Memory SafetyRisk of StackOverflow/Memory pressureControlled (via bounded queues and pool limits)
Lifecycle ControlManual (join, stop, etc.)Automated (shutdown, shutdownNow, awaitTermination)

Key takeaways

1
Java Executor Service and Thread Pools is a core concept in Concurrency that every Java developer should understand to build high-performance, stable applications.
2
Decoupling task submission from execution allows you to tune performance without changing business logic.
3
Always use bounded queues in production to avoid the 'Silent Killer'—OutOfMemoryError from an overloaded Task Queue.
4
Implement a clean shutdown hook to ensure in-flight tasks are completed or logged before the application closes.
5
Leverage custom ThreadFactories to provide meaningful names to your threads, ensuring easier debugging in production thread dumps.

Common mistakes to avoid

5 patterns
×

Using Executors.newFixedThreadPool() for bursty traffic without monitoring

Symptom
The application becomes unresponsive under high load. Heap dumps show millions of Runnable objects. No error is logged until OutOfMemoryError.
Fix
Replace with new ThreadPoolExecutor with a bounded queue (e.g., new LinkedBlockingQueue<>(500)) and a CallerRunsPolicy. Monitor queue depth with Micrometer.
×

Confusing execute() vs submit() — using execute() swallows unchecked exceptions

Symptom
Tasks fail silently. The thread is replaced with a new one, but no exception is logged. You only notice when expected side effects don't happen.
Fix
Use submit() and call get() on the returned Future, or override afterExecute() in ThreadPoolExecutor to log exceptions. Wrap tasks in try-catch if using execute().
×

Forgetting to name threads with a custom ThreadFactory

Symptom
Thread dumps show 'pool-1-thread-1', 'pool-2-thread-2'. Impossible to tell which pool is causing issues during an incident.
Fix
Implement a ThreadFactory that sets a meaningful name prefix like 'order-processor-'. Use the factory when constructing ThreadPoolExecutor.
×

Calling shutdown() inside a task submitted to the same pool

Symptom
The application hangs. The task waits for the pool to shut down, but the pool waits for the task to finish. Deadlock.
Fix
Never shutdown a pool from within a task submitted to it. Manage lifecycle externally (e.g., using @PreDestroy in Spring or a lifecycle hook).
×

Assuming corePoolSize or maxPoolSize is the only determinant of throughput

Symptom
Performance tuning attempts have no effect because the bottleneck is elsewhere (e.g., downstream service, disk I/O).
Fix
Understand the actual bottleneck before scaling threads. Profile the application to see where time is spent. Use formulas based on wait/compute ratio.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain the internal working of a ThreadPoolExecutor. What happens when ...
Q02SENIOR
What is a 'Saturation Policy' (RejectedExecutionHandler) and which one w...
Q03SENIOR
How does the corePoolSize differ from maximumPoolSize in a production co...
Q04SENIOR
What are the dangers of using Executors.newCachedThreadPool() for I/O bo...
Q05SENIOR
How do you handle exceptions thrown by tasks submitted to an ExecutorSer...
Q06JUNIOR
What is the difference between shutdown() and shutdownNow()? Which one i...
Q01 of 06SENIOR

Explain the internal working of a ThreadPoolExecutor. What happens when a new task is submitted?

ANSWER
When a task is submitted via execute(): 1. If the number of active threads < corePoolSize, a new thread is created to run the task. 2. Else, the task is placed into the work queue. 3. If the queue is full and active threads < maximumPoolSize, a new thread is created. 4. If the queue is full and active threads == maximumPoolSize, the RejectedExecutionHandler (saturation policy) is invoked. The pool also manages idle threads — those exceeding corePoolSize are terminated after keepAliveTime if no tasks arrive.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the difference between Java Executor Service and Thread Pool?
02
Can I reuse an ExecutorService after calling shutdown()?
03
How do I choose between newFixedThreadPool, newCachedThreadPool, and newScheduledThreadPool?
04
What happens if I don't shutdown the executor?
05
How do I monitor thread pool health in production?
🔥

That's Concurrency. Mark it forged?

5 min read · try the examples if you haven't

Previous
Java Threads and Runnable Explained
2 / 6 · Concurrency
Next
synchronized Keyword in Java