Blog Concurrency and Multithreading

Java Executor Framework: Mastering in Multithreading Efficiency

Before going to ExecutorFramework first understand what is Asynchronous execution and Concurrent execution.

Asynchronous Execution: In Java Execution Framework we can submit the task for execution without waiting for their completion. The task summited to the Execution Framework is to continue to run separate threads, without being blocked.

This means your main thread doesn’t have to wait for tasks to finish their execution, the main thread can continue its execution.

This is particularly useful in scenarios where tasks may take a long time to complete, such as I/O operations, network requests, or computationally intensive operations.

Concurrent execution: In concurrent Execution, Java Executor Framework can execute multiple tasks simultaneously, leveraging multiple threads or cores available on the system.

By utilizing thread pools and managing the concurrent execution of tasks, the Executor Framework utilizes efficient system resources and can significantly improve the overall performance of an application.

What is the Executor Framework?

The Executor Framework is a powerful and essential utility in Java for efficiently managing and executing asynchronous tasks concurrently using multithreading. It was introduced in Java 5 as part of the java.util.concurrent package, providing a standardized and robust way to create, schedule, and control the execution of tasks in a separate thread pool.

At the core of the Executor Framework lies the concept of thread pools, which allow developers to manage and reuse a group of worker threads efficiently. By leveraging thread pools, developers can avoid the overhead of creating and destroying threads for every task, leading to improved resource utilization and reduced overhead.

The Executor Framework offers several key components:

  1. ExecutorService: This interface defines the methods for submitting and managing tasks, as well as controlling the lifecycle of the thread pool.
  2. ThreadPoolExecutor: A commonly used implementation of the ExecutorService interface, providing a flexible and configurable thread pool.
  3. Callable and Future: These interfaces enable developers to submit tasks that return results and handle their completion asynchronously.
  4. ScheduledExecutorService: An extension  ExecutorService that allows scheduling tasks to run at a specific time or with a recurring delay.

With the Executor Framework, developers can easily configure thread pools with various policies and strategies, such as fixed or cached thread pools, and customize thread creation, task queuing, and rejection handling. This level of control and flexibility ensures optimal resource allocation and task execution based on application requirements.

Additionally, the framework supports advanced features like task prioritization, cancellation, and exception handling, making it a comprehensive solution for complex multithreaded scenarios.

By leveraging the Java Executor Framework, developers can write more efficient, scalable, and maintainable multithreaded applications, benefiting from improved performance, reduced resource consumption, and simplified thread management

What is the Executors Class?

The Executors class is a factory class introduced in Java 5 as part of the java.util.concurrent package. It provides a set of static factory methods for creating various types of ExecutorService instances, which represent thread pools. By encapsulating the complexities of thread pool creation and configuration, the Executors class simplifies the process of working with concurrent programming in Java.

Key Factory Methods in the Executors Class

  1. newFixedThreadPool(int nThreads): Creates a thread pool with a fixed number of threads. This pool is suitable for applications with a relatively stable workload and a bounded number of concurrent tasks.
  2. newCachedThreadPool(): Creates a thread pool that creates new threads as needed but reuses previously constructed threads when available. This pool is useful for applications with a dynamic workload and a large number of short-lived tasks.
  3. newSingleThreadExecutor(): Creates an executor that uses a single worker thread to execute tasks sequentially. This is useful for serializing task execution in a multithreaded environment.
  4. newScheduledThreadPool(int corePoolSize): Creates a thread pool that can schedule tasks to run after a given delay or periodically. This is useful for executing time-based or recurring tasks.
  5. newWorkStealingPool(int parallelism): Creates a work-stealing thread pool that efficiently distributes tasks among available processors, utilizing parallelism for improved performance on multi-core systems (Java 8+).

Benefits of Using the Executors Class

  1. Simplified Thread Pool Creation: The Executors class eliminates the need for developers to manually create and configure thread pools, reducing boilerplate code and potential errors.
  2. Standardized Thread Pool Configurations: By providing predefined configurations for common use cases, the Executors class promotes consistency and best practices across applications.
  3. Improved Performance and Resource Utilization: The provided thread pool implementations are optimized for various workload scenarios, ensuring efficient resource allocation and task execution.
  4. Encapsulation and Abstraction: The Executors class encapsulates the complexities of thread pool management, allowing developers to focus on their application logic rather than low-level threading details.

Getting Started with the Executors Class

Using the Executors class is straightforward. Here’s an example of creating a fixed thread pool and submitting tasks for execution:

Java
package concurrent.execution_framework;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class WebServer{

    private static final int THREAD_POOL_SIZE = 50;

    public static void main(String [] args){
        ExecutorService executorService = Executors.newFixedThreadPool(THREAD_POOL_SIZE);

        //Simulate a stream of client requests
        for(int i=0; i< 100; i++){
            int requestId = i;
            executorService.submit(() -> handleClientRequest(requestId) );
        }
        // Shutdown the executor service
        executorService.shutdown();
    }
    private static void handleClientRequest(int requestId){
        System.out.println("Start handling client request " + requestId +"on thread" + Thread.currentThread().getName());
        try{
            Thread.sleep(2000); // Simulate time to handle client request
        } catch(InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("Finished handling client request "+ requestId);
    }
}

//OUTPUT
Start handling client request 37on threadpool-1-thread-38
Start handling client request 3on threadpool-1-thread-4
Start handling client request 4on threadpool-1-thread-5
Start handling client request 18on threadpool-1-thread-19
Start handling client request 26on threadpool-1-thread-27
Start handling client request 47on threadpool-1-thread-48
Start handling client request 2on threadpool-1-thread-3
Start handling client request 38on threadpool-1-thread-39
........
...
Finished handling client request 92
Finished handling client request 69
Finished handling client request 96

In this example, we’re simulating a web server that receives a stream of client requests. Each client request is handled by a separate task using the ExecutorService. The handleClientRequest method simulates the time it takes to handle a client request.

This is a practical example of how you can use the ExecutorService to handle multiple tasks concurrently in a real-world application. The ExecutorService manages the threads for you, so you don’t have to manually create and manage threads. This makes your code simpler and more efficient.

The Executor Framework in Java, combined with the convenience of the Executors class, provides a powerful and flexible solution for managing and executing concurrent tasks. By leveraging the predefined thread pool configurations offered by the Executors class, developers can focus on their application logic while benefiting from improved performance, resource utilization, and simplified concurrent programming.

.

The ExecutorService is an extension of the Executor interface in Java’s java.util.concurrent package, offering a more versatile set of utilities for asynchronous task execution. It manages a pool of threads for executing Runnable and Callable tasks provides a way to track the completion of tasks and allows for the shutdown of the executor. Let’s delve into the key methods ExecutorService and illustrate them with examples.

Key Methods of ExecutorService

1. submit

  • Submits a task for execution and returns a Future representation of the task’s pending results.
  • Overloaded to accept Runnable, Callable with a result.
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
public class TestExecutors {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        Future<Integer> futureTask = executorService.submit(() ->{
            return 123;
        });
        try {
            Integer result = futureTask.get();
            System.out.println("Result of Task 1:"+ result);
        }catch (InterruptedException | ExecutionException e){
            e.printStackTrace();
        }
        executorService.shutdown();
    }
}
//Result of Task 1:123

2. invokeAll

  • On the other hand, the invokeAll method is designed to execute a collection of Callable tasks and return a list of Future objects representing the results of all tasks. It does not directly return the results of the tasks because it is intended to execute all the tasks concurrently and provide a way to retrieve their results asynchronously.
  • The primary purpose of invokeAll is to execute multiple tasks concurrently and obtain the results of all of them. By returning a list of Future objects, it allows you to handle the results of individual tasks separately, either by calling Future.get() to retrieve the result or by using additional methods provided by the Future interface
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class ExecutorServiceInvokeAllDemo {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        List<Callable<String>> tasks = Arrays.asList(
                () -> "Task 1",
                () -> "Task 2",
                () -> "Task 3"
        );

        try {
            List<Future<String>> results = executor.invokeAll(tasks);

            for (Future<String> resultFuture : results) {
                System.out.println("Result: " + resultFuture.get());
            }
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        } finally {
            executor.shutdown(); 
        }
    }
}

3. invokeAny

Executes the given collection of Callable tasks and returns the result of one of the successfully executed tasks (the first to complete successfully).

It does not return a Future object because it is intended to block and wait for one of the tasks to complete, after which it returns the result of that task directly.

The primary purpose of invokeAny is to execute multiple tasks concurrently and obtain the result of any one of them, whichever completes first. This is useful in scenarios where you have multiple ways to perform a task, and you want to get the result from the fastest or most efficient way.

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.*;

public class ExecutorServiceInvokeAnyDemo {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        List<Callable<String>> tasks = Arrays.asList(
                () -> "Task 1",
                () -> "Task 2",
                () -> "Task 3"
        );
        try{
            String result = executor.invokeAny(tasks);
            System.out.println("Result:"+ result);

        }catch (InterruptedException | ExecutionException e){
            e.printStackTrace();
        }
        finally {
            executor.shutdown();
        }
    }
}

4. shutdown

Initiates an orderly shutdown in which previously submitted tasks are executed, but no new tasks are accepted.

ExecutorService executor = Executors.newFixedThreadPool(2);
// Submit tasks
executor.shutdown(); // Disallows further submissions and finishes existing tasks

5. shutdownNow

Attempts to stop all actively executing tasks, halts the processing of waiting tasks, and returns a list of the tasks that were awaiting execution.

ExecutorService executor = Executors.newFixedThreadPool(2);
// Submit tasks
List<Runnable> notExecutedTasks = executor.shutdownNow(); // Attempts to stop all executing tasks

6. isShutdown & isTerminated

Returns true if the executor has been shut down. Returns true if all tasks have completed following shut down.

ExecutorService executor = Executors.newFixedThreadPool(2);
// Submit tasks
executor.shutdown();
System.out.println("Is shutdown: " + executor.isShutdown());
System.out.println("Is terminated: " + executor.isTerminated());

7. awaitTermination

Blocks until all tasks have completed execution after a shutdown request, or the timeout occurs, or the current thread is interrupted

ExecutorService executor = Executors.newFixedThreadPool(2);
// Submit tasks
executor.shutdown();
try {
    if (!executor.awaitTermination(1, TimeUnit.HOURS)) {
        executor.shutdownNow();
    }
} catch (InterruptedException e) {
    executor.shutdownNow();
}

These examples showcase the versatility and power of the ExecutorService interface for managing and controlling the execution of asynchronous tasks in Java applications. By understanding and utilizing these methods, developers can create efficient, scalable, and robust concurrent applications.

Java Executor Framework offers several benefits:

  1. Improved Performance: By reusing threads from a pool, the overhead of thread creation and destruction is minimized, leading to better performance and resource utilization.
  2. Scalability: Thread pools can dynamically adjust their size based on the workload, ensuring optimal resource allocation and task execution.
  3. Simplified Thread Management: The framework abstracts away the complexities of thread creation, scheduling, and management, allowing developers to focus on their application logic.
  4. Task Decoupling: Asynchronous task execution decouples the task submission from the processing logic, enabling better code organization and separation of concerns.
  5. Robust Exception Handling: The framework provides mechanisms for handling exceptions thrown by tasks, ensuring proper error handling and application stability.
  6. Task Scheduling: With the ScheduledExecutorService, developers can schedule tasks to run at specific times or with recurring delays, enabling advanced scheduling scenarios.
Avatar

Neelabh

About Author

As Neelabh Singh, I am a Senior Software Engineer with 6.6 years of experience, specializing in Java technologies, Microservices, AWS, Algorithms, and Data Structures. I am also a technology blogger and an active participant in several online coding communities.

You may also like

Blog Design Pattern

Understanding the Builder Design Pattern in Java | Creational Design Patterns | CodeTechSummit

Overview The Builder design pattern is a creational pattern used to construct a complex object step by step. It separates
Blog Tech Toolkit

Base64 Decode

Base64 encoding is a technique used to encode binary data into ASCII characters, making it easier to transmit data over