Executor Thread Pools Methods
The `java.util.concurrent` package provides several methods to create & manage Thread Pools through the `Executors` class. Here are some commonly used methods:
1. `newFixedThreadPool(int nThreads)`: Creates a Thread Pool with a fixed number of worker threads. If all threads are busy & a new task is submitted, it waits in a queue until a thread becomes available.
2. `newCachedThreadPool()`: Creates a Thread Pool that creates new threads as needed, but reuses previously constructed threads when they are available. Idle threads are kept for 60 seconds before being terminated & removed from the pool.
3. `newSingleThreadExecutor()`: Creates a Thread Pool with only one worker thread. This ensures that tasks are executed sequentially in the order they are submitted.
4. `newScheduledThreadPool(int corePoolSize)`: Creates a Thread Pool that can schedule tasks to run after a given delay or periodically.
Let’s look at an example demonstrating the usage of `newCachedThreadPool()`:
Java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CachedThreadPoolExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
Runnable task = new Task(i);
executorService.execute(task);
}
executorService.shutdown();
}
}
class Task implements Runnable {
private int taskId;
public Task(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
System.out.println("Task " + taskId + " is running on thread " + Thread.currentThread().getName());
}
}

You can also try this code with Online Java Compiler
Run Code
Output
Task 1 is running on thread pool-1-thread-2
Task 3 is running on thread pool-1-thread-4
Task 7 is running on thread pool-1-thread-8
Task 9 is running on thread pool-1-thread-10
Task 6 is running on thread pool-1-thread-7
Task 5 is running on thread pool-1-thread-6
Task 4 is running on thread pool-1-thread-5
Task 8 is running on thread pool-1-thread-9
Task 2 is running on thread pool-1-thread-3
Task 0 is running on thread pool-1-thread-1
In this example, we create a `CachedThreadPool` using `Executors.newCachedThreadPool()`. The Thread Pool creates new threads as needed to handle the submitted tasks. If an idle thread is available when a task is submitted, it is reused. This flexibility allows the Thread Pool to adapt to the workload dynamically.
Thread Pool Example
Let's look at a more practical example of using a Thread Pool in Java. Suppose we have a list of files that need to be processed. We can use a Thread Pool to process these files concurrently, improving the overall performance.
Java
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FileProcessingExample {
public static void main(String[] args) {
// Create a list of files to process
List<File> files = new ArrayList<>();
files.add(new File("file1.txt"));
files.add(new File("file2.txt"));
files.add(new File("file3.txt"));
// Create a fixed-size Thread Pool with 3 worker threads
ExecutorService executorService = Executors.newFixedThreadPool(3);
// Submit file processing tasks to the Thread Pool
for (File file : files) {
Runnable task = new FileProcessor(file);
executorService.execute(task);
}
// Shutdown the Thread Pool after all tasks are submitted
executorService.shutdown();
}
}
class FileProcessor implements Runnable {
private File file;
public FileProcessor(File file) {
this.file = file;
}
@Override
public void run() {
// Process the file
System.out.println("Processing file: " + file.getName());
// Simulate file processing time
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("File processed: " + file.getName());
}
}

You can also try this code with Online Java Compiler
Run Code
Output
Processing file: file2.txt
Processing file: file1.txt
Processing file: file3.txt
File processed: file1.txt
File processed: file2.txt
File processed: file3.txt
In this example, we have a list of files that need to be processed. We create a fixed-size Thread Pool with 3 worker threads using `Executors.newFixedThreadPool(3)`. We then iterate over the list of files & submit a `FileProcessor` task for each file to the Thread Pool using `executorService.execute(task)`.
The `FileProcessor` class implements the `Runnable` interface & represents the task of processing a file. Inside the `run()` method, we simulate file processing by printing a message & adding a delay of 2 seconds using `Thread.sleep(2000)`.
The Thread Pool manages the execution of these tasks concurrently, utilizing the available worker threads. As soon as a worker thread completes processing a file, it becomes available to handle the next file in the queue.
Risks in using Thread Pools
While Thread Pools has many advantages, just like any other tool, they have some risks also, like:
1. Thread Leakage: If tasks submitted to the Thread Pool have uncaught exceptions or don't complete properly, the threads may become stuck, leading to thread leakage. This can eventually cause the Thread Pool to run out of available threads, resulting in a program that hangs or crashes.
2. Resource Thrashing: If the Thread Pool is not sized appropriately or if tasks are not efficiently designed, it can lead to resource thrashing. This happens when threads compete for limited resources, such as CPU or I/O, leading to excessive context switching & reduced overall performance.
3. Deadlocks: Improper use of thread synchronization or resource locking within tasks can lead to deadlocks. If two or more tasks are waiting for each other to release resources, it can result in a deadlock, where the tasks are stuck indefinitely.
4. Starvation: If tasks with lower priority or longer execution times are continuously added to the Thread Pool, they may starve other tasks of CPU time. This can happen if the Thread Pool is not configured to handle tasks fairly, leading to some tasks being delayed or never executed.
To mitigate these risks, it's important to follow best practices when using Thread Pools:
- Properly handle exceptions within tasks to prevent thread leakage.
- Size the Thread Pool appropriately based on the system resources & expected workload.
- Design tasks efficiently to minimize resource contention & lengthy operations.
- Use thread synchronization mechanisms, such as locks & semaphores, carefully to avoid deadlocks.
- Consider using a fair task execution policy or a priority-based queue to prevent starvation.
Let’s look at an example of how to handle exceptions within a task:
class ExceptionHandlingTask implements Runnable {
@Override
public void run() {
try {
// Task logic that may throw an exception
throw new IllegalArgumentException("Example exception");
} catch (Exception e) {
// Handle the exception gracefully
System.err.println("Exception occurred: " + e.getMessage());
}
}
}
In this example, the task logic is enclosed within a try-catch block. If an exception occurs, it is caught & handled gracefully by printing an error message. This prevents the exception from propagating & causing thread leakage.
Important Points
When working with Thread Pools in Java, there are several important points to keep in mind:
1. Thread Pool Sizing: Choosing the appropriate size for a Thread Pool is crucial. If the pool is too small, it may lead to underutilization of system resources & increased task queuing. If the pool is too large, it may lead to excessive context switching & reduced performance. Consider factors such as the number of available cores, expected task duration, & system load when determining the optimal pool size.
2. Task Submission: Tasks can be submitted to a Thread Pool using the `execute()` method for simple Runnable tasks or the `submit()` method for Callable tasks that return a result. The `submit()` method returns a `Future` object, which can be used to retrieve the result or check the status of the task.
3. Task Completion: To ensure proper completion of tasks & graceful shutdown of the Thread Pool, it's important to call the `shutdown()` method after all tasks have been submitted. This prevents new tasks from being accepted & allows previously submitted tasks to complete. The `awaitTermination()` method can be used to wait for all tasks to finish before proceeding further.
4. Monitoring: Monitoring the Thread Pool is essential to ensure its health & performance. The `ThreadPoolExecutor` class provides methods to monitor the pool's status, such as `getActiveCount()` to get the number of currently executing tasks, `getCompletedTaskCount()` to get the number of completed tasks, & `getQueue()` to access the task queue.
5. Exception Handling: Proper exception handling within tasks is crucial to prevent thread leakage & ensure the stability of the Thread Pool. Exceptions should be caught & handled appropriately within the task's `run()` method. Uncaught exceptions can cause threads to abruptly terminate, leading to resource leaks.
Here's an example showing some of these important points in a code, to clear any doubt of yours:
Java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolMonitoringExample {
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor executorService = (ThreadPoolExecutor) Executors.newFixedThreadPool(3);
// Submit tasks to the Thread Pool
for (int i = 0; i < 5; i++) {
executorService.submit(new Task(i));
}
// Monitor the Thread Pool
while (true) {
System.out.println("Active Threads: " + executorService.getActiveCount());
System.out.println("Completed Tasks: " + executorService.getCompletedTaskCount());
System.out.println("Task Queue Size: " + executorService.getQueue().size());
if (executorService.getActiveCount() == 0 && executorService.getQueue().isEmpty()) {
break;
}
TimeUnit.SECONDS.sleep(1);
}
// Shutdown the Thread Pool
executorService.shutdown();
executorService.awaitTermination(1, TimeUnit.MINUTES);
}
}
class Task implements Runnable {
private int taskId;
public Task(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
System.out.println("Task " + taskId + " is running.");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task " + taskId + " completed.");
}
}

You can also try this code with Online Java Compiler
Run Code
Output
Task 0 is running.
Task 1 is running.
Task 2 is running.
Active Threads: 3
Completed Tasks: 0
Task Queue Size: 2
Task 0 completed.
Task 3 is running.
Task 1 completed.
Task 4 is running.
Active Threads: 3
Completed Tasks: 2
Task Queue Size: 0
Task 2 completed.
Active Threads: 2
Completed Tasks: 3
Task Queue Size: 0
Task 3 completed.
Active Threads: 1
Completed Tasks: 4
Task Queue Size: 0
Task 4 completed.
Active Threads: 0
Completed Tasks: 5
Task Queue Size: 0
In this example, we create a fixed-size Thread Pool with 3 worker threads & submit 5 tasks to it. We then monitor the Thread Pool using a loop that prints the number of active threads, completed tasks, & the size of the task queue. The loop continues until all tasks are completed & the task queue is empty.
After all tasks are completed, we shut down the Thread Pool using the `shutdown()` method & wait for up to 1 minute for all tasks to finish using the `awaitTermination()` method.
Tuning Thread Pools
Tuning a Thread Pool involves adjusting its configuration to optimize performance based on the specific requirements of the application. Here are some key aspects to consider when tuning a Thread Pool:
1. Pool Size: The size of the Thread Pool determines the maximum number of threads that can be active simultaneously. Setting the pool size too low may lead to underutilization of system resources, while setting it too high may result in excessive context switching & overhead. A good starting point is to set the pool size equal to the number of available CPU cores. However, this may need to be adjusted based on the specific workload & characteristics of the tasks.
2. Queue Size: The queue size determines the maximum number of tasks that can be queued waiting for an available thread. If the queue size is too small, tasks may be rejected if the queue is full. If the queue size is too large, it may consume excessive memory & lead to increased latency. The appropriate queue size depends on the expected task arrival rate & the processing time of each task.
3. Rejection Policy: When the Thread Pool & queue are full, the rejection policy determines how new tasks are handled. Common rejection policies include:
- `AbortPolicy`: Throws a `RejectedExecutionException` if a task is rejected.
- `CallerRunsPolicy`: The calling thread itself executes the rejected task.
- `DiscardPolicy`: Silently discards the rejected task without executing it.
- `DiscardOldestPolicy`: Discards the oldest unhandled task in the queue & tries to resubmit the new task.
4. Keep-Alive Time: For Thread Pools that dynamically adjust their size, such as the `CachedThreadPool`, the keep-alive time specifies how long idle threads should be kept in the pool before being terminated. Setting the keep-alive time too short may result in frequent thread creation & destruction, while setting it too long may lead to resource wastage.
Let’s look at an example of creating a custom Thread Pool with customised parameters:
Java
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class TunedThreadPoolExample {
public static void main(String[] args) {
int corePoolSize = 5;
int maximumPoolSize = 10;
long keepAliveTime = 60;
TimeUnit unit = TimeUnit.SECONDS;
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(100);
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
new ThreadPoolExecutor.CallerRunsPolicy()
);
// Submit tasks to the tuned Thread Pool
for (int i = 0; i < 50; i++) {
executor.execute(new Task(i));
}
executor.shutdown();
}
}
class Task implements Runnable {
private int taskId;
public Task(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
System.out.println("Task " + taskId + " is running.");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task " + taskId + " completed.");
}
}

You can also try this code with Online Java Compiler
Run Code
Output
Task 2 is running.
Task 4 is running.
Task 0 is running.
Task 1 is running.
Task 3 is running.
Task 4 completed.
Task 1 completed.
Task 5 is running.
Task 0 completed.
Task 7 is running.
Task 3 completed.
Task 8 is running.
Task 6 is running.
Task 2 completed.
Task 9 is running.
Task 5 completed.
Task 7 completed.
Task 10 is running.
Task 11 is running.
Task 8 completed.
Task 12 is running.
Task 6 completed.
Task 13 is running.
Task 9 completed.
Task 14 is running.
Task 10 completed.
Task 11 completed.
Task 15 is running.
Task 12 completed.
Task 16 is running.
Task 17 is running.
Task 13 completed.
Task 18 is running.
Task 14 completed.
Task 19 is running.
Task 15 completed.
Task 20 is running.
Task 16 completed.
Task 17 completed.
Task 21 is running.
Task 22 is running.
Task 18 completed.
Task 23 is running.
Task 19 completed.
Task 24 is running.
Task 20 completed.
Task 25 is running.
Task 22 completed.
Task 21 completed.
Task 27 is running.
Task 23 completed.
Task 28 is running.
Task 26 is running.
Task 24 completed.
Task 29 is running.
Task 25 completed.
Task 30 is running.
Task 27 completed.
Task 31 is running.
Task 28 completed.
Task 32 is running.
Task 26 completed.
Task 33 is running.
Task 29 completed.
Task 34 is running.
Task 30 completed.
Task 31 completed.
Task 35 is running.
Task 32 completed.
Task 36 is running.
Task 33 completed.
Task 37 is running.
Task 34 completed.
Task 38 is running.
Task 39 is running.
Task 35 completed.
Task 36 completed.
Task 40 is running.
Task 41 is running.
Task 37 completed.
Task 42 is running.
Task 38 completed.
Task 39 completed.
Task 43 is running.
Task 44 is running.
Task 40 completed.
Task 41 completed.
Task 45 is running.
Task 46 is running.
Task 42 completed.
Task 47 is running.
Task 43 completed.
Task 48 is running.
Task 44 completed.
Task 49 is running.
Task 45 completed.
Task 46 completed.
Task 47 completed.
Task 49 completed.
Task 48 completed.
In this example, we create a custom `ThreadPoolExecutor` with tuned parameters. We set the core pool size to 5, the maximum pool size to 10, the keep-alive time to 60 seconds, & the queue size to 100. We also specify the `CallerRunsPolicy` as the rejection policy.
We then submit 50 tasks to the tuned Thread Pool. The Thread Pool will dynamically adjust its size based on the workload, up to the maximum pool size. If the queue is full & all threads are busy, the `CallerRunsPolicy` will cause the calling thread to execute the rejected task directly.
Tuning the Thread Pool requires careful consideration of the application's requirements, workload characteristics, & system resources. It may involve iterative testing & monitoring to find the optimal configuration for the specific use case.
Frequently Asked Questions
What happens if all threads in the Thread Pool are busy & a new task is submitted?
If all threads are busy, the new task is added to the work queue. If the queue is full, the rejection policy determines how the task is handled.
Can a Thread Pool be resized dynamically during runtime?
Yes, the ThreadPoolExecutor class allows dynamic resizing of the pool using methods like setCorePoolSize() & setMaximumPoolSize().
How can I gracefully terminate a Thread Pool?
To gracefully terminate a Thread Pool, call the shutdown() method, which allows previously submitted tasks to be completed. Use awaitTermination() to wait for all tasks to finish.
Conclusion
In this article, we discussed Java Thread Pools, a powerful mechanism for managing & optimizing concurrent task execution. We learned about the benefits of using Thread Pools, such as improved performance, resource management, & simplified programming. We explained different methods provided by the Executors class to create Thread Pools & examined a practical example of processing files concurrently using a Thread Pool.
You can also practice coding questions commonly asked in interviews on Coding Ninjas Code360.
Also, check out some of the Guided Paths on topics such as Data Structure and Algorithms, Competitive Programming, Operating Systems, Computer Networks, DBMS, System Design, etc., as well as some Contests, Test Series, and Interview Experiences curated by top Industry Experts.