Using Java's Future and ExecutorService
See how Future and ExecutorService work independently and how they can be combined to solve issues with intense calculations, background tasks, and more.
Join the DZone community and get the full member experience.
Join For FreeIn this blog, I will show you the basics of using Java Future and Executor Service. Combining the following...
java.util.concurrent.Future<V>
...and...
java.util.concurrent.ExecutorService
...can be a very useful mechanism for solving tasks that have to be computed for a short time and processing the result of the calculation further in the logic. In this article, I am going to give you an example and some notes about using them in concurrent programming.
General Information
Future <V> is an interface that represents the result of an asynchronous computation. Once the computation is finished, you can obtain the result of it by using the get() method. Bear in mind that this is a blocking operation and waits until the outcome (V) is available.
Calling get() could take a considerable amount of time. Instead of wasting time, you can apply two approaches. The first one is using get() as well, but setting a timeout value as a parameter that will prevent you from getting stuck if something goes awry. The second way is by using the isDone() method, which takes a quick look at the Future and checks if it has finished its work or not.
ExecutorService represents an abstraction of thread pools and can be created by the utility methods of the Executors class. These methods can initialize a number of executors depending on the purpose they will be used for. There are several ways to delegate a task to ExecutorService:
–execute(Runnable) – returns void and cannot access the result.
–submit(Runnable or Callable<T>) – returns a Future object. The main difference is that when submitting Callable<T>, the result can be accessed via the returned Future object.
–invokeAny(Collection<Callable<T>>) – returns the result of one of the Callable objects that finished its work successfully. The rest of the tasks are canceled.
–invokeAll(Collection<Callable<T>>)) – returns a list of Future objects. All tasks are executed and the outcome can be obtained via the returned result list.
Last, when all tasks have finished their work, the threads in ExecutorService are still running. They are not destroyed yet and are in a “standby” mode. This will make the JVM keep running. For the purpose of bypassing this problem, Java offers you two methods – shutdown() and shutdownNow(). The key difference between them is the stopping of ExecutorService.
shutdown() will not stop it immediately and will wait for all running threads to finish. Meanwhile, ExecutorService will not accept new tasks. On the other hand, shutdownNow() will try to stop it immediately. It will try to instantly stop all running tasks and to skip the processing of the waiting ones. The method returns a list of all running tasks for which there are no guarantees when they will be stopped.
Code Example
In order to illustrate the written above, I have prepared a simple code demo. To start with, we have a class called CalculationTask, implementing:
Callable <Result>
Callable is an interface that stands for a task that returns a result after some computations. This class contains our business logic, and every time a task starts, the call() method is executed. In our case, it contains calculations that take a lot of time to be completed.
CalculationTask
package eu.dreamix.calculator;
import java.util.concurrent.Callable;
public class CalculationTask implements Callable<Result> {
private final BigDecimal invoiceId;
public CalculationTask(BigDecimal invoiceId) {
this.invoiceId = invoiceId;
}
@Override
public Result call() throws Exception {
Result result = null;
...
// Calculations for which it takes a long time be completed
...
return result;
}
}
The next class, whose implementation I have not shown below because it is irrelevant for the example, is Result. It holds the result type of computation returned by call().
InvoiceCalculatorClient
This is the palace where we create an Executor service with a fixed number of threads. A separate task is created for every element in a list and is submitted it to the executor. Then, we get the result of computation with the help of the returned Future object in order to operate with it as we need it.
package eu.dreamix.calculator.bad;
import java.math.BigDecimal;
import java.util.ArrayList;
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;
import eu.dreamix.calculator.CalculationTask;
import eu.dreamix.calculator.Result;
public class InvoiceCalculatorClient {
public static void main(String[] args) {
List<Result> invoiceCalculationsResult = new ArrayList<Result>();
ExecutorService executor = Executors.newFixedThreadPool(THREADS_COUNT);
// invoiceIDs is a collection of Invoice IDs
for (BigDecimal invoiceID : invoiceIDs) {
Callable<Result> task = new CalculationTask(invoiceID);
Future<Result> future = executor.submit(task);
Result calculationResult = null;
try {
calculationResult = future.get();
} catch (InterruptedException | ExecutionException e) {
// ... Exception handling code ...
}
invoiceCalculationsResult.add(calculationResult);
}
executor.shutdown();
}
}
If you observe the example carefully, you will see a small issue in the for-loop that can cause a big problem in the future. (Hint: get() is a blocking operation). It is obvious that get() is called right away, submitting a task to the executor. This means that the next task will not start its work before the previous task finishes, and there is no effect of a multithreading approach in this case.
...
List<Future<Result>> futuresList = new ArrayList<>();
// invoiceIDs is a collection of Invoice IDs
for (BigDecimal invoiceID : invoiceIDs) {
Callable<Result> task = new CalculationTask(invoiceID);
Future<Result> future = executor.submit(task);
futuresList.add(future);
}
for (Future<Result> future : futuresList) {
Result calculationResult = null;
try {
calculationResult = future.get();
} catch (InterruptedException | ExecutionException e) {
// ... Exception handling code ...
}
invoiceCalculationsResult.add(calculationResult);
}
...
In the example above, the first thing that we do is to start the tasks and to save the Future result. When we reach future.get(), it is very likely to have a nearly ready result. This time, it depends on the thread count and the logic of the task. Either with complex logic or a reasonable thread count, the fix shows an acceptable speed-up.
Another idea for a solution is creating a collection of Callables<V> objects and using invokeAll(Collection<Callable>)).
List<Callable<Result>> callables = new ArrayList<>();
callables.add(new CalculationTask(invoiceID));
executorService.invokeAll(callables);
Final Words
The combination of Future and ExecutorService is a powerful instrument for background task execution because of its flexibility makes it suitable for:
Time-consuming calculations
Calling web services or accessing remote services/resources
Working with large-sized data structures
Published at DZone with permission of Kostadin Hamanov, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments