Module 2 - Futures
Module Overview
Explore Callable interfaces and Future objects for asynchronous programming, enabling efficient non-blocking operations in your Java applications.
Learning Objectives
- Use Future's get method to retrieve the result of an asynchronous task
- Implement code to handle exceptions thrown while retrieving the result of an asynchronous task from a Future
- Implement a method that retrieves the result of an asynchronous task from a Future with minimal blocking time
- Recall that the Future object represents the eventual result of a concurrent task and will not immediately contain a result
- Summarize the Callable interface
- Outline the differences between the Callable and Runnable interfaces
- Implement the Callable interface using lambda syntax
- Implement functionality that executes a task asynchronously by submitting a Callable to an ExecutorService
- Design and implement a class that implements Callable to execute functionality concurrently
Understanding the Callable Interface
The Callable interface represents a task that returns a result and can throw an exception. This makes it more powerful than Runnable, which cannot return a value or throw checked exceptions.
Key differences between Callable and Runnable:
- Callable can return results while Runnable cannot
- Callable can throw checked exceptions while Runnable cannot
- Callable is more suitable for tasks that compute values
// Implementing Callable using lambda syntax
Callable<String> task = () -> {
// Some computation that returns a value
return "Task completed successfully";
};
// Submitting a Callable to an ExecutorService
ExecutorService executor = Executors.newCachedThreadPool();
Future<String> future = executor.submit(task);
Overview
In this lesson, we will learn about two new concurrency tools, the Callable and Future interfaces. We will use them to execute tasks with return values in concurrent threads and get those values after those threads have completed. We will learn how to use them with an ExecutorService from the previous lesson.
The first reading focuses on using Callables for concurrent programming. We'll be explaining the difference between a Callable and a Runnable, and how to use them with an ExecutorService.
The second reading will focus on Futures. We'll explain how they wrap the results of concurrent tasks and how to safely use them.
The Callable Interface
Up until now we've been using the Runnable interface in our concurrency lessons. Runnables, when used in threading, allow us to execute code concurrently to our application's main thread. They can be executed by instantiating a Thread with them directly or, as we learned in the last lesson, submitted to an ExecutorService. However, Runnables don't have return values, so threads executing a Runnable can't return a value upon completion. That's where the Callable interface comes in.
The Callable interface is conceptually the same as Runnable. They both can be used to execute tasks in a concurrent thread. The major difference is that Callables return a value on completion. The return type is defined in the class declaration where we implement Callable, like this ...implements Callable<ReturnType>. The Callable interface has a method named call() which contains the code to be executed by the thread. This is similar to the run() method in the Runnable interface. The call() method does two things different from run(). It can return a value of the type defined in our declaration and it can throw checked exceptions, neither of which are possible with a Runnable.
One of the limitations of Callables is that we can't use them to instantiate a Thread the way we did with Runnable. However, we can still use them with ExecutorService. When we submit() a Callable to an ExecutorService, the ExecutorService wraps the result of call() in a Future. Futures provide a way to access the return value of a thread that may not have completed yet. We will learn more about Futures in the next reading.
Combining Callables with an ExecutorService allows us to execute many tasks at once and to combine their results easily without having to share states between threads like we have done with Runnables.
How to Use Callables
In the code below, we have a program that is simulating charging a credit card. We have a class CardVerification that implements Callable and returns a Boolean. The call() method checks that the card number passed in starts with a specific sequence, and returns if it was verified or not. In the CardChargerManager class, we pass in cardNum to CardVerification and submit it to an ExecutorService. Then the result of the call is printed.
public class CardVerification implements Callable<Boolean> {
private String cardNum;
/**
* Constructor for CardVerification.
* @param num The card number to verify.
*/
public CardVerification(String num) {
cardNum = num;
}
/**
* Method call() is called on execution.
* @return Returns a boolean on completion.
*/
public Boolean call() {
System.out.println("CardVerification called.");
return cardNum.substring(0, 4).equals("1234");
}
}
public class CardChargerManager {
private String cardNum = "1234-5678-8910";
/**
* Method called when a credit card is charged.
*/
public void chargeCard() throws ExecutionException, InterruptedException {
ExecutorService verificationExecutor = Executors.newCachedThreadPool();
CardVerification verify = new CardVerification(cardNum);
Future<Boolean> cardNumValid = verificationExecutor.submit(verify);
System.out.println("cardNum verification result: " + cardNumValid.get());
}
}
Output:
CardVerification called.
cardNum verification result: true
This might seem like a lot, so let's go through it piece by piece. First, we implement Callable in CardVerification. It has a similar structure to Runnable, with some small changes. The first difference is in the declaration line.
public class CardVerification implements Callable<Boolean> {
For Callable interfaces we need to declare a return type, in this case, a Boolean. Then we implement the call() method that needs to return that same type.
public Boolean call() {
System.out.println("CardVerification called.");
return cardNum.substring(0, 4).equals("1234");
}
As we've discussed, the call() method serves the same function as run() does for Runnable, the primary difference being that call() has a return value. We should note that both call() and run() don't accept any parameters, so any required values need to be set in a constructor or setters before call() is executed. In this example, cardNum is passed into the constructor. Note that call() can also be called like a regular method directly in your code like run(). But, as you'll remember, that doesn't execute it in a new thread. That's why we need to submit CardVerification to an ExecutorService.
In the next section, the chargeCard() method is doing a lot of things. We will step through what's happening, but much of the code here we will discuss more in the next reading.
public void chargeCard() throws ExecutionException, InterruptedException {
ExecutorService verificationExecutor = Executors.newCachedThreadPool();
CardVerification verify = new CardVerification(cardNum);
Future<Boolean> cardNumValid = verificationExecutor.submit(verify);
System.out.println("cardNum verification result: " + cardNumValid.get());
}
First, create an ExecutorService to run our task. Then, we create a new CardVerification instance and pass in cardNum. This is our Callable instance we want to run in a new thread. To do that, we call submit() on the ExecutorService, passing in our Callable instance. submit() will wrap the result of the task as a Future of the same type declared by the Callable interface. So, when submit() is called here with our CardVerification class, a Future<Boolean> result is returned and stored in the variable cardNumValid.
Lastly, we use the result in our output by calling cardNumValid.get(). In order to call get() we need to handle two exceptions: ExecutionException and InterruptedException. In this example we handled them in the method declaration. These can be thrown by Future's get() method, but we'll go more into what can cause them in the next reading.
Callable lambdas
It should be noted that Callable tasks can also be created as lambdas, like we did with Runnable tasks in the last lesson. In the example below, we took the code above and modified it to demonstrate how this would work.
public class CardChargerManager {
private List<String> cardNumbers = new ArrayList<String>(Arrays.asList("1234", "5678", "9010"));
/**
* Method called when a credit card is charged.
*/
public void chargeCards() throws ExecutionException, InterruptedException {
ExecutorService verificationExecutor = Executors.newCachedThreadPool();
List<Future<Boolean>> cardValidations = new ArrayList<Future<Boolean>>();
for (String cardNumber : cardNumbers) {
cardValidations.add(
verificationExecutor.submit(() -> cardNumber.substring(0,4).equals("1234")));
}
for (Future<Boolean> isValid : cardValidations) {
if (!isValid.get()) {
//Bad Request.
}
}
}
}
In this modification of the CardChargeManager class we have an ArrayList of cardNumbers that we will be validating. The purpose of ExecutorService is to allow us to create many threads very quickly to handle many concurrent tasks, so we'll be creating a new Callable for each card number in our ArrayList.
Now, when the chargeCards() method is called, it iterates through our list of card numbers and creates a new callable lambda for each one. That lambda call returns its Future<Boolean> right into a new ArrayList of cardValidations that correspond to whether the card number is valid or not. You can see that the lambda function contains the same validation code that our Callable CardVerification class had. In this case it's only a few lines long rather than an entire separate class! Once all the threads are created, we loop through the cardValidations list to see if any of the cards were invalid. If they were, we'd respond appropriately here, probably by throwing an exception that indicates a bad request was made.
You should note that since we're creating the lambda as part of the CardChargerManager class, we are able to use cardNumbers as a shared value. Since we are not making changes to cardNumbers, this is OK. If we were making changes, we would need to be more careful, and would probably want to give each thread their own copy of the value they were validating, which would be better suited to our previous example with a separately implemented class.
Summary
In this reading, we learned about the Callable interface. We learned the differences between Runnable and Callable. We also learned how the call() method returns a result on completion and can throw checked exceptions. We also learned how to use ExecutorService's submit() method to execute a Callable in a separate thread and store the result in a Future. We were able to not only execute Callable tasks in a separate thread, but also retrieve the results of them this way.
Up Next
In the next reading, we will be covering the Future we get when we submit a Callable to an ExecutorService. We'll learn how to retrieve the result of the Callable from the Future and how to handle exceptions that can occur while retrieving that result.
Working with Future Objects
A Future represents the result of an asynchronous computation. It provides methods to check if the computation is complete, wait for its completion, and retrieve the computation result.
When working with Future objects, remember:
- The Future doesn't immediately contain the result
- Calling get() will block until the result is available
- You can use isDone() to check if a task is complete without blocking
// Retrieving results from a Future with exception handling
Future<String> future = executor.submit(task);
try {
// Block until the result is available
String result = future.get();
System.out.println("Result: " + result);
} catch (InterruptedException e) {
// Thread was interrupted
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
// Task threw an exception
System.err.println("Task failed: " + e.getCause());
} finally {
executor.shutdown();
}
Overview
In this lesson, we will learn about two new concurrency tools, the Callable and Future interfaces. We will use them to execute tasks with return values in concurrent threads and get those values after those threads have completed. We will learn how to use them with an ExecutorService from the previous lesson.
The first reading focused on using Callables for concurrent programming. We explained the difference between Callable and Runnable, and how to use Callables with an ExecutorService.
This reading will focus on the Futures. We'll explain how they wrap the results of concurrent tasks, and how to safely use them.
The Future interface
In the previous reading, we learned about the Callable interface and how we use it to submit tasks to an ExecutorService in order to get a result from a concurrent thread. Now we want to dig deeper into how those results are returned wrapped in a Future. The Future object is not a direct value, it just represents the "future" result of a concurrent task. This means that when the object is created and has a task assigned to it, there isn't immediately a result in it. Only after the task is complete are we able to retrieve the result from the Future.
This behavior makes working with Futures a little tricky. Future has a method get() which is how we get the result of the task. Complications can come up when calling get(). On creation, a Future will not have a value we can access. At this point it is only representative of the result since the associated task will not have completed. Meaning if we tried to call get() on a Future immediately after we create it, it's likely the result won't yet exist, so our main thread will be blocked. The get() method, when called, will stop (or "block") the execution of the main thread until the result referenced by a Future is available. We'll be looking at how to retrieve a result using get() in the next section.
It's important to understand, especially now with Future, that we are dealing with concurrent execution. When we submit or implement code in a concurrent thread, we largely lose control over the timing or order of our code's execution. With Future, we have the means to work with values from tasks that may not have completed yet. Many of the examples we've been working with in these lessons are simple enough that they can complete almost immediately. But in practical applications, you may be working with concurrent systems where tasks take longer, and it will be important to take these precautions to maximize the value of concurrency and avoid problems.
Using Futures
In this code snippet, we are making a simulation of an online cart checkout. We have two Callable inline lambdas that simulate network calls and return Booleans that confirm if the call was made or not. Both are submitted to the ExecutorService in checkout(), and a message is printed reporting if the transaction was successful.
public class CartCheckout {
private final PaymentService paymentService;
private final DistroService distroService;
private final AmazonUser user;
public CartCheckout(AmazonUser user, PaymentService paymentService, DistroService distroService) {
this.user = user;
this.paymentService = paymentService;
this.distroService = distroService;
}
/**
* Called when user goes to complete checkout for an order.
* @return Boolean whether all checkout tasks completed successfully
* @throws ExecutionException if calling get() throws an exception.
* @throws InterruptedException if this thread was interrupted while waiting.
*/
public Boolean checkout() throws ExecutionException, InterruptedException {
ExecutorService cartExecutor = Executors.newCachedThreadPool();
List<Future<Boolean>> networkCallResults = new ArrayList<Future<Boolean>>();
networkCallResults.add(
cartExecutor.submit(() -> paymentService.cardApproved(user.cardNum)));
networkCallResults.add(
cartExecutor.submit(() -> distroService.newOrderSubmitted(user.address)));
cartExecutor.shutdown();
boolean checkoutSuccessful = true;
for (Future<Boolean> result : networkCallResults) {
if (!result.get()) {
checkoutSuccessful = false;
}
}
return checkoutSuccessful;
}
}
To start, our CartCheckout class constructor accepts our dependencies, setting the AmazonUser, PaymentService and DistroService we need for our checkout process. These will be used by our Callable lambdas to handle parts of the checkout process concurrently. We've declared them final in order to ensure that the objects they reference won't change while threads are using them.
In the checkout() method, we create an ExecutorService and submit() the two Callable lambda tasks, storing the returned Futures in list networkCallResults.
List<Future<Boolean>> networkCallResults = new ArrayList<Future<Boolean>>();
networkCallResults.add(
cartExecutor.submit(() -> paymentService.cardApproved(user.cardNum)));
networkCallResults.add(
cartExecutor.submit(() -> distroService.newOrderSubmitted(user.address)));
Once we have our tasks submitted and our list of results in the networkCallResults list, we can call shutdown() on the ExecutorService since we don't require it anymore. We then get() the result of each of the Futures in our list to confirm each checkout task completed successfully, then return that result. Let's look more closely at the loop where we check the results in our Futures.
boolean checkoutSuccessful = true;
for (Future<Boolean> result : networkCallResults) {
if (!result.get()) {
checkoutSuccessful = false;
}
}
The important code is the result.get() call. When using Futures, it is a best practice to submit our tasks as early as possible and retrieve their results as late as possible. This minimizes the time we block on the get() method waiting for those tasks to complete. Here we shut down the ExecutorService before starting the check loop, which buys us some time for the tasks to complete. In a real-world example, the network calls might take longer than the time we give them here. This would result in our code being blocked until all the results came back.
Using exceptions with Future
The get() method in the example above can throw two exceptions, InterrruptedException and ExecutionException. An InterruptedException can be thrown if the thread calling get() is interrupted while waiting for get() to return. An ExecutionException wraps any exceptions thrown while the Callable is executing. Depending on what your program is doing you may need to handle these in specific ways so you can recover from them gracefully. In the code snippet below we added a try/catch to the check loop from the example above. We log the error, mark that checkout was not successful, and then continue the execution of our program.
boolean checkoutSuccessful = true;
for (Future<Boolean> result : networkCallResults) {
try {
if (!result.get()) {
checkoutSuccessful = false;
}
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
checkoutSuccessful = false;
}
}
get() Timeouts
There are other methods in Future that are worth knowing. One example is the get() method has an overload for adding a time limit on retrieving results. This overload takes the timeout duration and a time unit and throws a TimeoutException if the task has not returned a value before the timeout duration expires. This is useful when you want to set a limit on how long your main thread can block while waiting for your other tasks to complete. The example below shows how to use this, including the exception.
boolean checkoutSuccessful = true;
for (Future<Boolean> result : networkCallResults) {
try {
if (!result.get(1000, TimeUnit.MILLISECONDS)) {
checkoutSuccessful = false;
}
} catch (TimeoutException e) {
System.out.println("Network call timed out!");
checkoutSuccessful = false;
}
}
Conclusion
In this lesson, we covered the Callable and Future interfaces. We learned how Futures wrap the results of Callable tasks submitted to an ExecutorService, and how we can properly access those values with minimal interruption. These functions allow us to be a lot more efficient with our concurrent programming. We can use Callable tasks to return values back to our main thread from concurrent threads, spreading out the computation of large tasks and combining the results at the end. The Future objects created by the ExecutorService allow us to store and pass along the results of our concurrent tasks even before they've completed. This gives us the flexibility to let a concurrent task run its course while continuing with the processing in our main thread. When we need it, the Future will either have the value or pause our main thread until the value is ready. By submitting tasks early and retrieving the results after our other computations are complete, we can maximize our processing with many threads and minimize the time we spend waiting for those threads to complete.
Advanced Future Features
Futures provide several additional capabilities that help you control asynchronous task execution, such as timeouts, cancellation, and checking completion status.
Important Future methods:
get(long timeout, TimeUnit unit)
- Wait for result with a timeoutcancel(boolean mayInterruptIfRunning)
- Attempt to cancel executionisCancelled()
- Check if task was cancelledisDone()
- Check if task completed (successfully, with exception, or cancelled)
// Using Future with timeout
Future<String> future = executor.submit(task);
try {
// Wait only 2 seconds for the result
String result = future.get(2, TimeUnit.SECONDS);
System.out.println("Result: " + result);
} catch (TimeoutException e) {
// Task took too long, cancel it
future.cancel(true);
System.out.println("Task timed out and was cancelled");
} catch (Exception e) {
// Handle other exceptions
e.printStackTrace();
} finally {
executor.shutdown();
}