Java’s java.util.concurrent
package provides a robust framework for managing threads. Two commonly used thread pools from the Executors
class are newCachedThreadPool()
and newFixedThreadPool(int nThreads)
. In this blog, we will delve into the differences, use cases, examples, and best practices for these thread pools.
Overview
1. newCachedThreadPool()
- Characteristics:
- Creates a thread pool with potentially unlimited threads.
- Threads are created as needed and reused when available.
- Idle threads are terminated after 60 seconds.
- Uses a
SynchronousQueue
, meaning no task queueing; tasks are handed directly to threads.
- When to Use:
- For many short-lived asynchronous tasks.
- When you want minimal overhead for idle threads.
- In scenarios where the workload is bursty and unpredictable.
- Risks:
- Unbounded thread creation can lead to resource exhaustion under heavy workloads.
- Not ideal for long-running tasks or controlled concurrency.
2. newFixedThreadPool(int nThreads)
- Characteristics:
- Creates a thread pool with a fixed number of threads (
nThreads
). - Excess tasks are queued in an unbounded
LinkedBlockingQueue
until threads are available. - Threads remain alive indefinitely unless the pool is explicitly shut down.
- Creates a thread pool with a fixed number of threads (
- When to Use:
- For stable workloads with predictable concurrency requirements.
- For long-running tasks or controlled resource usage.
- Risks:
- An unbounded task queue can grow excessively, causing
OutOfMemoryError
.
- An unbounded task queue can grow excessively, causing
Key Differences
Feature | newCachedThreadPool | newFixedThreadPool |
---|---|---|
Thread Count | Dynamically adjusts (0 to unbounded). | Fixed (nThreads ). |
Idle Thread Time | Terminates after 60 seconds of inactivity. | Threads live indefinitely. |
Queueing | No queueing (uses SynchronousQueue ). | Tasks are queued if all threads are busy. |
Use Case | Short-lived bursty tasks. | Long-running predictable tasks. |
Risk | Unbounded threads can overwhelm resources. | Unbounded queue can exhaust memory. |
Examples
1. Example: Using newCachedThreadPool()
This example demonstrates handling short-lived tasks with newCachedThreadPool()
.
package com.codinginterviewprep.corejava.multithreading.executors;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
public class TestCachedThreadPoolExample {
// ANSI color codes
private static final String[] COLORS = {
"\u001B[31m", // Red
"\u001B[32m", // Green
"\u001B[33m", // Yellow
"\u001B[34m", // Blue
"\u001B[35m", // Purple
"\u001B[36m", // Cyan
"\u001B[37m", // White
"\u001B[91m", // Bright Red
"\u001B[92m", // Bright Green
"\u001B[93m" // Bright Yellow
};
private static final String RESET = "\u001B[0m"; // Reset color
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
executorService.execute(() -> {
String threadName = Thread.currentThread().getName();
int threadId = threadName.hashCode() % COLORS.length; // Generate a thread ID based on hash
if (threadId < 0) threadId += COLORS.length; // Handle negative hash values
String color = COLORS[threadId]; // Assign a color based on thread ID
System.out.println(color + threadName + " is executing a task" + RESET);
try {
Thread.sleep(1000);
} catch (InterruptedException exception) {
Thread.currentThread().interrupt();
}
});
}
executorService.shutdown();
}
}
- Output: Threads are created dynamically and reused for subsequent tasks.

2. Example: Using newFixedThreadPool()
This example demonstrates handling a fixed number of threads with newFixedThreadPool()
.
package com.codinginterviewprep.corejava.multithreading.executors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestFixedThreadPoolExample {
// ANSI color codes
private static final String[] COLORS = {
"\u001B[31m", // Red
"\u001B[32m", // Green
"\u001B[33m" // Yellow
};
private static final String RESET = "\u001B[0m"; // Reset color
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
executorService.execute(() -> {
String threadName = Thread.currentThread().getName();
// Extract thread index (1-based) from the thread name, e.g., pool-1-thread-X
int threadId = Integer.parseInt(threadName.split("-")[3]) - 1; // Adjust to 0-based index
String color = COLORS[threadId % COLORS.length]; // Assign color based on thread index
System.out.println(color + threadName + " is executing a task" + RESET);
try {
Thread.sleep(2000);
} catch (InterruptedException interruptedException) {
Thread.currentThread().interrupt();
}
});
}
executorService.shutdown();
}
}
- Output: Only 3 threads execute tasks simultaneously, and the rest are queued.

Use Cases
newCachedThreadPool()
- Advantages:
- Ideal for short-lived, bursty tasks.
- Minimizes overhead by terminating idle threads.
- Example Scenarios:
- Processing lightweight HTTP requests.
- Asynchronous logging.
- Parallel computations with variable load.
newFixedThreadPool()
- Advantages:
- Predictable resource usage.
- Suitable for long-running tasks or stable workloads.
- Example Scenarios:
- Batch processing (e.g., image processing).
- Background jobs with known concurrency limits.
- Server handling a fixed number of simultaneous connections.
Best Practices
- Monitor Resource Usage:
- Use tools like JMX or external monitoring to ensure threads and memory are within safe limits.
- Bound the Queue:
- For
newFixedThreadPool
, consider wrapping it with aThreadPoolExecutor
and using a bounded queue to preventOutOfMemoryError
.
- For
- Graceful Shutdown:
- Always call
shutdown()
orshutdownNow()
to release resources after use.
- Always call
- Rejection Policies:
- Use a custom
ThreadPoolExecutor
to define how tasks should be handled when the pool is full (e.g.,CallerRunsPolicy
).
- Use a custom
- Thread Safety:
- Ensure tasks are thread-safe to avoid race conditions.
Custom ThreadPoolExecutor Example
For advanced control, use ThreadPoolExecutor
directly:
import java.util.concurrent.*;
public class CustomThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = new ThreadPoolExecutor(
2, // Core pool size
5, // Maximum pool size
1, TimeUnit.MINUTES,// Keep-alive time
new ArrayBlockingQueue<>(10), // Task queue
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy() // Rejection policy
);
for (int i = 0; i < 20; i++) {
executor.execute(() -> {
System.out.println(Thread.currentThread().getName() + " is executing a task");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
executor.shutdown();
}
}
- Features:
- Bounded queue to limit task backlog.
- Custom rejection policy to handle excess tasks.
Conclusion
Choosing between newCachedThreadPool()
and newFixedThreadPool()
depends on your application’s workload and concurrency requirements. Use newCachedThreadPool()
for short-lived, bursty tasks and newFixedThreadPool()
for controlled, long-running tasks. For advanced scenarios, leverage ThreadPoolExecutor
for fine-grained control over thread pool behavior.
By understanding these options, you can design efficient and robust multithreaded applications.