Executor and Callables Exercise
This exercise is about using the ExecutorService and Callable classes. It’s a little example to learn from. The problem we want to solve is to have more tasks running at the same time. We will use the ExecutorService to manage the tasks and the Callable interface to create the tasks. The Callable interface is similar to the Runnable interface, but it can return a result and throw a checked exception. That makes it suitable for returning a json result from an API etc.
Code along - and make it your own
Here’s an example in Java using Callable, Future, and ExecutorService. We’ll create three tasks using Callable that return a String. We submit these tasks to an ExecutorService, gather the resulting Future objects into a list, and then wait until all tasks are done before looping over the Futures to print the results.
Create a simple Java program with the following code and run it in IntelliJ. Try to understand how it works and then modify it to fit your needs.
Example Code
import java.util.List;
import java.util.concurrent.*;
public class CallableFutureExample {
public static void runTasks() {
// Create a fixed thread pool with 3 threads
ExecutorService executor = Executors.newFixedThreadPool(3);
// Create 3 Callable tasks and submit them to the executor
Callable<String> task1 = () -> {
TimeUnit.SECONDS.sleep(1); // Sleep for taskId seconds
return "Task " + 1 + " completed";
};
Callable<String> task2 = () -> {
TimeUnit.SECONDS.sleep(2); // Sleep for taskId seconds
return "Task " + 2 + " completed";
};
Callable<String> task3 = () -> {
TimeUnit.SECONDS.sleep(3); // Sleep for taskId seconds
return "Task " + 3 + " completed";
};
// Submit the task and add the Future to the list
List<Future<String>> futures = null;
try {
futures = executor.invokeAll(List.of(task1, task2, task3));
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
// Wait for all tasks to complete and retrieve their results
for (Future<String> future : futures) {
try {
// get() blocks until the result is available
String result = future.get();
System.out.println(result);
}
catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
// Shut down the executor service
executor.shutdown();
}
}
public class Main {
public static void main(String[] args) {
// Record the start time
long startTime = System.currentTimeMillis();
CallableFutureExample.runTasks();
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
System.out.println("Task runtime: " + duration + " milliseconds");
}
}
Explanation
This code demonstrates how to execute multiple asynchronous tasks using the Callable interface and Future objects in conjunction with an ExecutorService. Here’s an explanation of each section of the code:
1. Imports
import java.util.List;
import java.util.concurrent.*;
java.util.List: This is for handling lists ofFutureobjects, which represent the results of asynchronous tasks.java.util.concurrent.*: This imports classes from thejava.util.concurrentpackage, includingCallable,ExecutorService,Executors,TimeUnit, andFuture.
2. runTasks Method
This method demonstrates how to create a fixed thread pool with ExecutorService, submit multiple Callable tasks, and retrieve their results using Future.
3. Creating a Thread Pool
ExecutorService executor = Executors.newFixedThreadPool(3);
- The
ExecutorServiceis created with a fixed thread pool of 3 threads usingExecutors.newFixedThreadPool(3). This pool can run up to 3 tasks concurrently.
4. Creating Callable Tasks
Callable<String> task1 = () -> {
TimeUnit.SECONDS.sleep(1);
return "Task " + 1 + " completed";
};
Callable<String> task2 = () -> {
TimeUnit.SECONDS.sleep(2);
return "Task " + 2 + " completed";
};
Callable<String> task3 = () -> {
TimeUnit.SECONDS.sleep(3);
return "Task " + 3 + " completed";
};
- Three
Callable<String>tasks are defined using lambda expressions. Each task sleeps for a certain number of seconds (1,2, and3seconds) to simulate work, and then returns a completion message (e.g.,"Task 1 completed"). Callableis similar toRunnable, but it can return a result (in this case, aString).
5. Submitting Tasks with invokeAll
futures = executor.invokeAll(List.of(task1, task2, task3));
invokeAll(): This method is used to submit a collection ofCallabletasks (task1,task2,task3) to the executor service. It returns aList<Future<String>>, where eachFuturecorresponds to a task and allows you to retrieve its result.- Blocking Behavior: The
invokeAllmethod blocks until all tasks are finished. This means the main thread will wait for all three tasks to complete.
6. Retrieving Results from Futures
for (Future<String> future : futures) {
try {
String result = future.get();
System.out.println(result);
}
catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
- For each
Future<String>in thefutureslist, theget()method is called to retrieve the result of the correspondingCallabletask. get(): Blocks until the task is completed and the result is available.- Error Handling:
get()can throwInterruptedException(if the current thread is interrupted while waiting) orExecutionException(if the task throws an exception during execution), so these exceptions are caught and handled.
7. Shutting Down the Executor
executor.shutdown();
shutdown(): Initiates an orderly shutdown of the executor service, meaning no new tasks will be accepted, but previously submitted tasks will continue to be executed.
Full Execution Flow
- Three tasks (
task1,task2,task3) are created, each simulating work by sleeping for1,2, and3seconds, respectively. - The tasks are submitted to the
ExecutorServiceusinginvokeAll(), which waits for all tasks to complete. - Once all tasks finish, their results are printed to the console using
Future.get(). - The executor is gracefully shut down after all tasks are complete.
Example Output
Task 1 completed
Task 2 completed
Task 3 completed
The tasks complete in the order of their sleep durations. The first task (1-second sleep) completes first, followed by the second (2-second sleep), and finally the third (3-second sleep).
Benefits of this Approach
- Efficient Task Management: By using a fixed thread pool, the system can manage the number of concurrent threads efficiently.
- Simplified Handling of Asynchronous Tasks: With
CallableandFuture, you can manage asynchronous tasks that return values and easily retrieve their results once they finish. - Blocking Behavior Control:
invokeAll()ensures that the main thread waits for all tasks to complete before moving on, allowing for sequential result processing.