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 aRunnable
object to an executor. - The
ExecutorService
interface: This interface extends theExecutor
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 theExecutor
andExecutorService
interfaces and includes additional methods for managing the status and parameters of the executor. - The
Executors
class: for creatingExecutor
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 theisDone()
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 aTimeUnit
of that period. If the period ends and the task hasn't finished executing, the method throws aTimeoutException
.
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.