3. Java Multithreading: Advance Guide to ExecutorService and Callable
Advance Guide to ExecutorService and Callable with Examples
Managing threads can be streamlined using the ExecutorService and Callable interfaces. This guide will explore these components, making multithreading both accessible and manageable.
What is ExecutorService?
ExecutorService is a powerful tool in Java for managing and controlling thread execution. It abstracts the complexities of thread creation and management, offering a higher-level API for handling tasks.
Creating an ExecutorService
To begin using ExecutorService, you typically use the Executors class, which provides several factory methods for creating different types of thread pools.
Here’s a basic example using a fixed thread pool to perform concurrent operations on each laptop in a list using Runnable tasks.
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class LaptopListProcessor {
public static void main(String[] args) {
// List of laptops (simulated as strings for simplicity)
List<String> laptops = Arrays.asList("Laptop1", "Laptop2", "Laptop3", "Laptop4", "Laptop5");
// Create an ExecutorService with a fixed thread pool of 3 threads
ExecutorService executor = Executors.newFixedThreadPool(3);
// Submit tasks to process each laptop
for (String laptop : laptops) {
Runnable task = () -> processLaptop(laptop);
executor.submit(task);
}
// Shut down the executor service
executor.shutdown();
}
// processing a laptop
private static void processLaptop(String laptop) {
try {
// Simulate processing time
Thread.sleep(1000);
System.out.println(laptop + " processed successfully on " + Thread.currentThread().getName());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("Processing of " + laptop + " was interrupted.");
}
}
}
What is Callable:
While Runnable is great for tasks that don’t return results, Callable is more versatile. It allows tasks to return a result and handle exceptions, making it ideal for tasks that perform computations or need to return result.
Creating a Callable
Here's how you can define and use a Callable, an example to concurrently update the prices of a list of laptops and retrieve the updated prices
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class SimpleLaptopUpdater {
public static void main(String[] args) {
// List of laptops with their base prices
List<Laptop> laptops = Arrays.asList(
new Laptop("Laptop1", 1000),
new Laptop("Laptop2", 1500),
new Laptop("Laptop3", 2000)
);
// Create an ExecutorService with a fixed thread pool of 3 threads
ExecutorService executor = Executors.newFixedThreadPool(3);
// Process each laptop and collect futures
try {
for (Laptop laptop : laptops) {
// Create a Callable task to update the laptop's price
Callable<String> task = () -> {
// updating the laptop's price
double newPrice = laptop.getPrice() * 1.10;
laptop.setPrice(newPrice);
return laptop.getName() + " new price: " + newPrice;
};
// Submit the task to the executor
Future<String> future = executor.submit(task);
// Retrieve and print the result
try {
String result = future.get();
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
}
} finally {
// Shut down the executor service
executor.shutdown();
}
}
}
// Class to represent a laptop
class Laptop {
private String name;
private double price;
public Laptop(String name, double price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
}
Combining ExecutorService and Callable
You can combine ExecutorService with Callable to manage multiple tasks that return results. Here’s a more advanced example:
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;
public class ExecutorServiceAndCallableDemo {
public static void main(String[] args) {
// Create an ExecutorService with a fixed thread pool of 3 threads
ExecutorService executor = Executors.newFixedThreadPool(3);
// Create a list of Callable tasks
List<Callable<String>> tasks = new ArrayList<>();
for (int i = 1; i <= 5; i++) {
final int taskId = i;
tasks.add(() -> {
Thread.sleep(taskId * 1000); // Simulate varying durations
return "Result from task " + taskId;
});
}
try {
// Submit all tasks and get a list of Future objects
List<Future<String>> results = executor.invokeAll(tasks);
// Print results from each Future
for (Future<String> future : results) {
System.out.println(future.get());
}
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
// Shut down the executor
executor.shutdown();
}
}
Multiple Callable tasks are created and submitted to the executor. The invokeAll() method is used to submit a collection of tasks and return a list of Future objects, which are then used to retrieve and print the results.
Conclusion:
ExecutorService provides a flexible way to manage and execute tasks with thread pools, while Callable enhances Runnable by supporting result retrieval and exception handling. With these tools, you can efficiently handle concurrent tasks, making your Java applications more responsive and scalable. Whether you’re performing computations, managing asynchronous operations, or running parallel processes, ExecutorService and Callable offer a straightforward approach to effective multithreading.
Complete Code on GitHub
If you found this post helpful, consider subscribing for more updates or following me on LinkedIn, and GitHub. Have questions or suggestions? Feel free to leave a comment or contact us. Happy coding!!