Table of Contents
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:
- ExecutorService: This interface defines the methods for submitting and managing tasks, as well as controlling the lifecycle of the thread pool.
- ThreadPoolExecutor: A commonly used implementation of the
ExecutorService
interface, providing a flexible and configurable thread pool. - Callable and Future: These interfaces enable developers to submit tasks that return results and handle their completion asynchronously.
- 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
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.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.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.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.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
- 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. - Standardized Thread Pool Configurations: By providing predefined configurations for common use cases, the
Executors
class promotes consistency and best practices across applications. - Improved Performance and Resource Utilization: The provided thread pool implementations are optimized for various workload scenarios, ensuring efficient resource allocation and task execution.
- 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:
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 ofCallable
tasks and return a list ofFuture
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 ofFuture
objects, it allows you to handle the results of individual tasks separately, either by callingFuture.get()
to retrieve the result or by using additional methods provided by theFuture
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:
- Improved Performance: By reusing threads from a pool, the overhead of thread creation and destruction is minimized, leading to better performance and resource utilization.
- Scalability: Thread pools can dynamically adjust their size based on the workload, ensuring optimal resource allocation and task execution.
- Simplified Thread Management: The framework abstracts away the complexities of thread creation, scheduling, and management, allowing developers to focus on their application logic.
- Task Decoupling: Asynchronous task execution decouples the task submission from the processing logic, enabling better code organization and separation of concerns.
- Robust Exception Handling: The framework provides mechanisms for handling exceptions thrown by tasks, ensuring proper error handling and application stability.
- Task Scheduling: With the
ScheduledExecutorService
, developers can schedule tasks to run at specific times or with recurring delays, enabling advanced scheduling scenarios.