Core Java

Guide to ExecutorService vs. CompletableFuture

Modern applications often require efficient handling of concurrent tasks to improve performance and responsiveness. Java provides robust concurrency utilities, such as ExecutorService and CompletableFuture, to manage and coordinate asynchronous tasks. Let us delve into understanding Java ExecutorService vs CompletableFuture to see how they compare and complement each other in handling concurrency.

1. Java Concurrency with ExecutorService and CompletableFuture

1.1 Overview of ExecutorService

ExecutorService is a part of the Java concurrency framework introduced in Java 5. It provides a higher-level replacement for working with threads directly, offering a pool of threads that can be managed efficiently.

The main advantages of using ExecutorService are:

  • Managing a pool of threads efficiently
  • Submitting tasks for execution
  • Shutting down the service in a controlled manner

1.1.1 Responsibility

ExecutorService focuses on managing a pool of threads and task execution lifecycle, including scheduling and execution. It is responsible for:

  • Providing a pool of reusable threads
  • Submitting tasks for asynchronous execution
  • Handling task lifecycle (start, complete, and shutdown)

1.1.2 Code Example

The following code demonstrates the use of ExecutorService in Java to manage a pool of threads for executing tasks concurrently.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorServiceExample {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        
        for (int i = 0; i < 5; i++) {
            executorService.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " is executing task.");
            });
        }
        
        executorService.shutdown();
    }
}

It begins by importing the necessary classes from the java.util.concurrent package. In the main method, an ExecutorService is created using Executors.newFixedThreadPool(3), which initializes a thread pool with a fixed number of threads (in this case, 3). This means that at most 3 threads will be used to execute the submitted tasks concurrently.

A for loop is then used to submit 5 tasks to the executorService. Each task is defined using a lambda expression that prints the name of the thread executing the task. The submit method of ExecutorService is used to submit these tasks for execution. After all tasks have been submitted, the shutdown method of ExecutorService is called to initiate an orderly shutdown of the executor service. This means that the service will no longer accept new tasks, but it will complete all the submitted tasks before terminating.

1.2 Overview of CompletableFuture

CompletableFuture is a class introduced in Java 8 that represents a future result of an asynchronous computation. It provides a comprehensive API for asynchronous programming and supports non-blocking operations.

The key features of CompletableFuture include:

  • Combining multiple asynchronous tasks
  • Chaining tasks
  • Handling errors gracefully

1.2.1 Responsibility

CompletableFuture focuses on the outcome of asynchronous computations and provides a rich API for composition and error handling. It is responsible for:

  • Representing the result of an asynchronous computation
  • Providing methods to compose and combine futures
  • Handling exceptions and applying callbacks

1.2.2 Code Example

The following code demonstrates the use of CompletableFuture in Java to handle asynchronous computations.

import java.util.concurrent.CompletableFuture;

public class CompletableFutureExample {
    public static void main(String[] args) {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            return "Hello, World!";
        });
        
        future.thenAccept(System.out::println);
    }
}

It begins by importing the necessary class from the java.util.concurrent package. In the main method, a CompletableFuture object is created using the supplyAsync method, which takes a lambda expression as an argument. This lambda expression defines a task that returns the string “Hello, World!”.

The result of this asynchronous computation is represented by the CompletableFuture<String> object named future. Once the computation is complete, the thenAccept method is called on the future object. This method takes a Consumer (in this case, System.out::println) which is executed with the result of the computation, printing “Hello, World!” to the console.

2. Chaining Asynchronous Tasks

One of the powerful features of CompletableFuture is the ability to chain multiple asynchronous tasks together. This allows for more readable and maintainable code when dealing with complex asynchronous workflows.

The following code demonstrates how to chain multiple tasks using CompletableFuture in Java to create a sequence of asynchronous computations.

import java.util.concurrent.CompletableFuture;

public class ChainingTasksExample {
    public static void main(String[] args) {
        CompletableFuture.supplyAsync(() -> "Task 1")
            .thenApply(result -> result + " + Task 2")
            .thenApply(result -> result + " + Task 3")
            .thenAccept(System.out::println);
    }
}

It begins by importing the necessary class from the java.util.concurrent package. Within the main method, a series of tasks are chained together using the thenApply method, which applies a function to the result of the previous stage and returns a new CompletableFuture representing the result of the computation.

In this example, the initial task is created using supplyAsync, which returns a CompletableFuture representing a completed computation with the result “Task 1”. The thenApply method is then called to append ” + Task 2″ to the result of the first task, and another thenApply is used to append ” + Task 3″ to the result of the second task. Finally, the thenAccept method is called to consume the result of the last task and print it to the console using System.out::println.

3. Error Handling

CompletableFuture provides several methods for handling exceptions that occur during the execution of asynchronous tasks. You can use methods like exceptionally, handle, and whenComplete to handle errors gracefully.

The following code demonstrates how to handle errors gracefully when working with CompletableFuture in Java.

import java.util.concurrent.CompletableFuture;

public class ErrorHandlingExample {
    public static void main(String[] args) {
        CompletableFuture.supplyAsync(() -> {
            if (true) throw new RuntimeException("Something went wrong!");
            return "Success";
        }).exceptionally(ex -> {
            System.out.println("Error: " + ex.getMessage());
            return "Fallback result";
        }).thenAccept(System.out::println);
    }
}

It begins by importing the necessary class from the java.util.concurrent package. Within the main method, a CompletableFuture is created using supplyAsync, which defines a task that throws an exception by using throw new RuntimeException("Something went wrong!") for demonstration purposes.

The exceptionally method is then called on the CompletableFuture object to handle any exceptions that occur during the execution of the asynchronous task. It takes a Function as an argument, which processes the exception and returns a fallback result. In this example, the function prints the error message to the console using System.out.println and returns a fallback result of “Fallback result”.

Finally, the thenAccept method is called to consume the result of the computation. If an exception occurs during the execution of the task, the exceptionally method ensures that the fallback result is returned instead of propagating the exception further. This allows for graceful error handling in asynchronous computations.

4. Timeout Management

Managing timeouts is crucial in asynchronous programming to avoid indefinitely waiting for a task to complete. CompletableFuture provides methods like orTimeout and completeOnTimeout to handle timeouts effectively.

The following code demonstrates how to manage timeouts when working with CompletableFuture in Java.

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

public class TimeoutManagementExample {
    public static void main(String[] args) {
        CompletableFuture.supplyAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                throw new IllegalStateException(e);
            }
            return "Result after delay";
        }).orTimeout(2, TimeUnit.SECONDS)
          .exceptionally(ex -> "Timeout occurred")
          .thenAccept(System.out::println);
    }
}

It begins by importing the necessary classes from the java.util.concurrent package, including CompletableFuture and TimeUnit. Within the main method, a CompletableFuture is created using supplyAsync, which defines a task that simulates a delay of 5 seconds using TimeUnit.SECONDS.sleep(5).

The orTimeout method is then called on the CompletableFuture object to specify a timeout duration of 2 seconds. If the task is not completed within this time frame, a TimeoutException is thrown. In this example, the exceptionally method is used to handle the timeout exception by returning a fallback result of “Timeout occurred”.

Finally, the thenAccept method is called to consume the result of the computation. If the task is completed within the specified timeout duration, the result is printed to the console. Otherwise, the fallback result indicating a timeout is printed instead.

5. Conclusion

Java’s ExecutorService and CompletableFuture are powerful tools for managing concurrency in modern applications. They simplify the complexity of asynchronous programming by providing easy-to-use APIs for task management, chaining, error handling, and timeout management. By understanding and utilizing these utilities, developers can write efficient, responsive, and maintainable concurrent applications.

Yatin Batra

An experience full-stack engineer well versed with Core Java, Spring/Springboot, MVC, Security, AOP, Frontend (Angular & React), and cloud technologies (such as AWS, GCP, Jenkins, Docker, K8).
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button