Lesson 4
Customizing Thread Pool Executors
Customizing Executors

Welcome back! In the first lesson, we learned about Thread Pools and Executor Services, and how they allow efficient task execution by managing multiple threads. In this lesson, we'll be learning about the ThreadPoolExecutor, a versatile tool for optimizing thread usage in Java applications. Understanding how to customize ThreadPoolExecutor will empower you to manage your system's resources effectively.

What You'll Learn

By the end of this lesson, you will learn how to:

  • Set up a customizable ThreadPoolExecutor with flexible parameters like core and maximum pool sizes.
  • Implement a RejectedExecutionHandler to handle tasks when the thread pool is overloaded.
  • Monitor the ThreadPoolExecutor to gather crucial statistics such as active thread count, task count, and pool size.

These skills will not only enhance your understanding of Java's concurrency framework but also prepare you for developing highly efficient and robust applications.

ThreadPoolExecutor: A Powerful Thread Management Tool

The ThreadPoolExecutor is a highly configurable and extensible thread pool implementation that provides more flexibility than the simpler ExecutorService implementations offered by the Executors class. While ExecutorService provides basic methods for creating thread pools, ThreadPoolExecutor allows you to fine-tune important parameters, such as:

  • The core pool size, which defines how many threads are kept alive even if they are idle.
  • The maximum pool size, which determines the upper limit of threads that can be created to handle an increased number of tasks.
  • The queue size, which dictates how many tasks can be waiting in line for execution.
  • A rejected execution handler, which defines how to handle tasks that cannot be accepted when the queue is full and the pool is at maximum capacity.

In addition, ThreadPoolExecutor provides useful methods to monitor the current state of the thread pool, allowing you to:

  • Retrieve the pool size (total number of threads).
  • Check the active thread count (how many threads are currently executing tasks).
  • Get the task count (the number of tasks that have been submitted).

This configurability makes ThreadPoolExecutor a powerful tool for efficiently managing system resources, especially in complex, multi-threaded applications.

Now, let's dive into the practical implementation of customizing a ThreadPoolExecutor.

Creating a Custom ThreadPoolExecutor

We start by creating a custom ThreadPoolExecutor that allows us to specify the number of core and maximum threads, queue size, and other parameters.

Java
1// Creating a custom ThreadPoolExecutor 2ThreadPoolExecutor executor = new ThreadPoolExecutor( 3 2, // core pool size 4 4, // maximum pool size 5 10, // keep-alive time 6 TimeUnit.SECONDS, // time unit for the keep-alive time (10 seconds) 7 new ArrayBlockingQueue<>(8), // work queue size 8 new CustomThreadFactory(), // custom thread factory for creating new threads 9 new CustomRejectedExecutionHandler() // custom handler for rejected tasks 10);

Here, we define a core pool size of 2 and a maximum pool size of 4. This means the thread pool will maintain 2 threads even if idle and can expand up to 4 threads to handle high demand. Tasks that exceed the pool's capacity are stored in an ArrayBlockingQueue with a size of 8. If both the pool and queue are full, tasks are managed by the custom RejectedExecutionHandler.

Additionally, a CustomThreadFactory is used to create new threads in a controlled manner, allowing you to set custom configurations such as thread naming conventions, priority, or daemon status. This ensures better control and monitoring of the threads created by the pool.

Submitting Tasks to the Executor

Next, we submit multiple tasks to the executor. Each task will be executed by a thread from the pool, and each task will print which thread is executing it.

Java
1// Submitting tasks 2for (int i = 1; i <= 10; i++) { 3 int taskId = i; 4 executor.submit(() -> { 5 System.out.println("Executing Task " + taskId + " by " + Thread.currentThread().getName()); 6 sleep(2000); // Simulate task processing 7 }); 8}

In this loop, 10 tasks are submitted for execution. Each task prints a message indicating which thread is handling it and then sleeps for 2 seconds, simulating a longer-running task.

Monitoring Executor

After submitting tasks, we can monitor the ThreadPoolExecutor to check its current state, such as the number of active threads, total task count, and overall pool size.

Java
1// Monitoring executor 2System.out.println("Active Threads: " + executor.getActiveCount()); 3System.out.println("Task Count: " + executor.getTaskCount()); 4System.out.println("Pool Size: " + executor.getPoolSize()); 5 6executor.shutdown();

Here, we use the following methods:

  • getActiveCount(): Shows how many threads are currently executing tasks.
  • getTaskCount(): Returns the total number of tasks that have been submitted, both completed and in-progress.
  • getPoolSize(): Provides the total number of threads currently in the pool, both active and idle.

Finally, we call shutdown() to ensure that the executor shuts down gracefully after completing all tasks.

Creating a Custom Thread Factory

To customize how threads are created and named, we implement a CustomThreadFactory. This allows us to easily identify threads by their names.

Java
1// Custom Thread Factory 2public class CustomThreadFactory implements ThreadFactory { 3 private int counter = 0; 4 5 @Override 6 public Thread newThread(Runnable r) { 7 return new Thread(r, "CustomThread-" + counter++); 8 } 9}

With this CustomThreadFactory, each thread is named sequentially (CustomThread-0, CustomThread-1, etc.), which makes it easier to track thread activity during debugging or logging.

Implementing a Custom RejectedExecutionHandler

When the thread pool is full and tasks cannot be executed immediately, they are rejected. To handle these cases, we implement a custom RejectedExecutionHandler.

Java
1// Custom Rejected Execution Handler 2public class CustomRejectedExecutionHandler implements RejectedExecutionHandler { 3 @Override 4 public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { 5 System.out.println("Task Rejected: " + r.toString()); 6 } 7}

The CustomRejectedExecutionHandler logs a message when a task is rejected due to the pool and queue being full. In a real-world scenario, you could add more sophisticated logic, such as rescheduling the task or logging it for future processing.

Why It Matters

Customizing ThreadPoolExecutor is essential for building scalable and efficient multi-threaded applications. By controlling thread pool parameters, queue sizes, and rejection policies, you ensure that your application:

  • Optimizes Resource Usage: Adjusting thread pools to meet your application's needs prevents resource exhaustion and improves performance.
  • Enhances Scalability: Fine-tuning the executor allows your application to efficiently handle varying loads, scaling up or down as needed.
  • Improves Reliability: Customizing rejection policies ensures your application remains robust and gracefully handles overload situations without crashing.

Understanding how to manage resources effectively through custom thread pools will make your applications more efficient, resilient, and able to handle varying levels of demand.

Now, let's move to the practice section, where you can apply these concepts and experience the benefits of a customized ThreadPoolExecutor firsthand.

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.