Understanding Executors in Java

Understanding Executors in Java

In concurrent programming, managing threads and their life cycles can be complex and error-prone. The executor framework helps developers to execute concurrent tasks without the need for creating and managing threads themselves. Instead, they can create tasks and dispatch them to the executor, which handles the creation and management of the required threads.

Executors include the elimination of the need to explicitly create Thread objects and reduction of the overhead of thread creation through the reuse of worker threads.

Basic Components of the Executor Framework

The executor framework consists of various interfaces and classes that implement all the functionality provided by executors. The basic components of the framework include:

  • The Executor interface: This is the basic interface of the executor framework. It only defines a method that allows the programmer to send a Runnable object to an executor.
  • The ExecutorService interface: This interface extends the Executor interface and includes more methods to increase the functionality of the framework. This includes the ability to execute tasks that return results, execute a list of tasks with a single method call, and finish the execution of an executor and wait for its termination.
  • The ThreadPoolExecutor class: This class implements the Executor and ExecutorService interfaces and includes additional methods for managing the status and parameters of the executor.
  • The Executors class: for creating Executor objects and related classes.

The Callable and Future Interfaces

The Callable and Future interfaces allow us to execute tasks that return results. When we send a Callable task to an executor, it returns an implementation of the Future interface that allows us to control the execution and status of the task and retrieve its result.

The Callable Interface

The Callable interface is similar to the Runnable interface, but it returns a result. The main characteristics of this interface include:

  • It's a generic interface with a single type parameter corresponding to the return type of the call() method.
  • It declares the call() method that will be executed by the executor when it runs the task. The method must return an object of the type specified in the declaration.
  • The call() method can throw any checked exception.

The Future Interface

The Future interface allows us to control the execution and status of a Callable task and retrieve its result. The main characteristics of this interface include:

  • We can cancel the execution of the task using the cancel() method, which has a boolean parameter to specify whether to interrupt the task if it's running.
  • We can check whether the task has been canceled with the isCanceled() method or has finished with the isDone() method.
  • We can retrieve the value returned by the task using the get() method. There are two variants of this method. The first one returns the value if the task has finished executing. If the task hasn't finished executing, the method suspends the execution thread until it finishes. The second variant takes a period of time and a TimeUnit of that period. If the period ends and the task hasn't finished executing, the method throws a TimeoutException.

The CompletionService Interface

The CompletionService interface is an extension of the ExecutorService interface that allows you to submit Callable or Runnable tasks and retrieve their Future objects once they are completed.

The CompletionService is designed to help with managing the Future objects returned by the executor. It allows you to retrieve the results in the order they are completed, regardless of the order they were submitted.

Here's an example that demonstrates the use of the CompletionService interface:


ExecutorService executor = Executors.newFixedThreadPool(5);
CompletionService<Integer> completionService = new ExecutorCompletionService<>(executor);

for (int i = 1; i <= 10; i++) {
    final int task = i;
    completionService.submit(() -> {
        TimeUnit.SECONDS.sleep(task);
        return task;
    });
}

for (int i = 0; i < 10; i++) {
    Future<Integer> future = completionService.take();
    System.out.println("Result: " + future.get());
}

executor.shutdown();

In this example, we create an ExecutorService with a fixed thread pool of size 5, and a CompletionService that is initialized with the ExecutorService.

We submit 10 Callable tasks to the CompletionService. Each task sleeps for a number of seconds equal to its value, and then returns that value.

We then retrieve the results from the CompletionService using the take() method, which blocks until a task is completed. The take() method returns the Future object associated with the completed task, which we can then use to retrieve the result.

Note that the results are retrieved in the order that the tasks are completed, not in the order they were submitted. This is one of the benefits of using a CompletionService.

Once all tasks have been completed, we shut down the ExecutorService.

CompletableFuture

Java 8 introduced a new class called CompletableFuture, which provides a powerful and flexible way to manage asynchronous computations. A CompletableFuture is a type of Future that can also be used as a Promise, meaning that you can manually set its result or exception value. It also provides a fluent API for combining multiple CompletableFuture instances and handling exceptions.

Here's an example of how to create a CompletableFuture and handle its result:


CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    // Simulate a long-running computation
    try {
        Thread.sleep(1000);
    } catch (InterruptedException ex) {
        Thread.currentThread().interrupt();
    }
    return "Hello, world!";
});

future.thenAccept(result -> {
    System.out.println("Result: " + result);
});

In this example, we create a CompletableFuture using the supplyAsync method, which takes a Supplier that returns a value. The supplyAsync method executes the Supplier in a separate thread and returns a CompletableFuture that will be completed with the result of the Supplier. We then attach a callback to the CompletableFuture using the thenAccept method, which takes a Consumer that will be called with the result when it is available.

You can also use the CompletableFuture class to create a chain of computations that depend on each other. Here's an example:


CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 42);

future.thenApply(i -> i * 2)
      .thenAccept(result -> {
          System.out.println("Result: " + result);
      });

In this example, we create a CompletableFuture that supplies the value 42. We then attach a transformation step to the CompletableFuture using the thenApply method, which takes a Function that transforms the value. Finally, we attach a callback to the resulting CompletableFuture using the thenAccept method, which prints the result.

CompletableFuture also provides methods for combining multiple CompletableFuture instances. For example, you can use the thenCombine method to combine two CompletableFuture instances and apply a transformation to their results:


CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> 2);
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> 3);

CompletableFuture<Integer> result = future1.thenCombine(future2, (i, j) -> i * j);

result.thenAccept(value -> {
    System.out.println("Result: " + value);
});

In this example, we create two CompletableFuture instances that supply the values 2 and 3. We then use the thenCombine method to combine these two CompletableFuture instances and apply a multiplication operation to their results. Finally, we attach a callback to the resulting CompletableFuture using the thenAccept method, which prints the result.

Overall, CompletableFuture provides a powerful and flexible way to manage asynchronous computations in Java. Its fluent API and support for callbacks and composition make it easy to create complex asynchronous workflows.

Using Threads in Spring

Concurrency is the ability to execute multiple tasks simultaneously. Spring provides several mechanisms to support concurrency, such as:

  • Task Execution and Scheduling: Spring provides a TaskExecutor interface that can be used to execute tasks asynchronously. It also provides a TaskScheduler interface that can be used to schedule tasks to run at specific times or intervals.
  • Spring Asynchronous Methods: Spring provides a way to make methods asynchronous using the @Async annotation. This annotation can be added to any method that needs to be executed asynchronously. When a method is marked as @Async, it is executed in a separate thread, allowing other methods to be executed concurrently.
  • Spring MVC Async Support: Spring provides support for asynchronous web applications using Spring MVC. This allows web requests to be handled asynchronously, freeing up threads to handle other requests.

Using Task Execution and Scheduling

TaskExecutor and TaskScheduler are two important interfaces provided by Spring for executing tasks asynchronously and scheduling tasks to run at specific times or intervals.

TaskExecutor is an interface that defines a single method, execute(Runnable task), which takes a Runnable object as an argument and executes it asynchronously in a separate thread. TaskExecutor can be used to execute tasks asynchronously in the background, freeing up the main thread to handle other tasks.

TaskScheduler is an interface that defines several methods for scheduling tasks to run at specific times or intervals. TaskScheduler can be used to schedule tasks to run periodically or at a specific time in the future.

Here's an example of using TaskExecutor to execute a task asynchronously:


public class MyTask implements Runnable {
   public void run() {
      // code to execute asynchronously
   }
}

public class MyService {
   private TaskExecutor executor;

   public MyService(TaskExecutor executor) {
      this.executor = executor;
   }

   public void executeMyTask() {
      executor.execute(new MyTask());
   }
}

Using Spring Asynchronous Methods

Spring provides a way to make methods asynchronous using the @Async annotation. When a method is marked as @Async, it is executed in a separate thread, allowing other methods to be executed concurrently. Here's an example:


@Service
public class MyService {
   @Async
   public void executeMyTask() {
      // code to execute asynchronously
   }
}

To use @Async, you need to enable it by adding @EnableAsync to your configuration class. Here's an example:


@Configuration
@EnableAsync
public class AppConfig {
   // configuration code
}

Using Spring MVC Async Support

Spring provides support for asynchronous web applications using Spring MVC. This allows web requests to be handled asynchronously, freeing up threads to handle other requests. Here's an example:


@RestController
public class MyController {
   @GetMapping("/async")
   public Callable<String> async() {
      return () -> {
         // code to execute asynchronously
         return "result";
      };
   }
}

In this example, the method async() returns a Callable object, which is executed asynchronously in a separate thread. The result of the task is returned as a string.