Table of Contents
Introduction🧑🏫
In Java, both the Runnable
and Callable
are @FunctionalInterface interfaces are used to execute tasks in a separate thread, but there are key differences between them, particularly in how they are used with threading and concurrent utilities like ExecutorService
.
Key Terminologies 📚
- Runnable 🏃♂️: An interface in Java that represents a task to be run by a thread. It has a single method,
run()
, that does not return any value and cannot throw checked exceptions. - Callable 📞: An interface in Java that represents a task that should return a result. It has a single method,
call()
, that returns a value and can throw checked exceptions. - ExecutorService 🧵: An interface in Java that provides methods to manage and control thread execution, such as starting, stopping, and monitoring progress.
- Future 🔮: Represents the result of an asynchronous computation. Methods are provided to check if the computation is complete, to wait for its completion, and to retrieve the result of the computation.
Key Difference between Callable and Runnable in Java:
Feature📋 | Callable 📞 | Runnable🏃♂️ |
---|---|---|
Interface📄 | java.util.concurrent.Callable<V> | java.util.Runnable |
Returns Value 📤 | Callable.call() returns a result of type V. It’s designed for tasks that need to compute a result. | Runnable.run() does not return a result. It’s designed for tasks that don’t need to return a result. |
Method🛠️ | V call() throws Exception | void run() |
Exception Handling 🚧 | Callable.call() can throw checked exceptions. This allows exceptions to be propagated to the calling thread. | Runnable.run() cannot throw any checked exceptions. Any exceptions that occur within the run() method must be handled inside the method itself. |
Usage with ExecutorService 🧵 | Callable tasks can be executed via ExecutorService.submit() . This method returns an Future object that can be used to retrieve the result of the Callable task when it’s ready. | Runnable tasks can be executed with ExecutorService.execute() or by instantiating a new Thread . |
Interaction with Future 🕰️ | When a Callable task is submitted to an ExecutorService , the Future object returned by submit() can be used to retrieve the result of the computation when it’s ready. [1] | When a Runnable task is submitted to an ExecutorService , the Future object returned by submit() does not provide a result. |
Explanation:
- Callable: Used when you need a thread to perform a task and return a result.
- Runnable: Used when you need a thread to perform a task without returning a result.
Example Use Cases:
- Callable: Calculating a Fibonacci number, fetching data from a database in a separate thread.
- Runnable: Updating a UI element, sending a network request in the background.
Let me know if you’d like any of the explanations expanded on!
Runnable Interface In Java🎯
Method to Implement: The Runnable
interface has a single method called run()
that you need to implement. This method does not return a value and does not throw checked exceptions🎯.
@Override
public void run() {
// Task code goes here
}
- Usage: It’s mainly used for tasks that do not return any value after completion.
- Exception Handling: Since
run()
cannot throw any checked exceptions, any exceptions that occur within therun()
method must be handled inside the method itself.
This means if your task involves operations that could throw checked exceptions (like I/O operations, parsing, etc.), you must handle these exceptions inside therun()
method itself.
Let’s go through an example to illustrate how to handle exceptions within aRunnable
task.
Example Scenario: Reading a File withRunnable
Imagine you have a task to read content from a file and print it to the console. Reading a file can throw aFileNotFoundException
(if the file does not exist) or anIOException
(if an I/O error occurs during reading). Both of these exceptions are checked exceptions, so they must be handled within therun()
method. - Returning Values: Cannot return a result. If you need to return a value after the task completion, you’d typically need to use some shared variables or other mechanisms outside of the
Runnable
interface.
Callable Interface In Java🤖
- Method to Implement: The
Callable
interface has a single methodcall()
that you need to implement. Unlikerun()
, thecall()
method can return a value and throw checked exceptions.
@Override
public V call() throws Exception {
// Task code goes here, with return value
}
- Usage: It’s used for tasks that are expected to return a result after completion. It is more versatile than
Runnable
because it can handle exceptions thrown during task execution and return values. - Exception Handling: The
call()
method can throw checked exceptions, allowing you to propagate exceptions to the calling thread, where they can be handled appropriately. - Returning Values: Can return a result of a generic type
V
after task completion. This is useful when your task execution needs to result in a specific object or value.
ExecutorService and Future
When you submit a Callable
task to an ExecutorService, you receive a Future object in return. This Future can be used to retrieve the result of the Callable task when it is completed, making it possible to handle asynchronous computations effectively.
On the other hand, submitting a Runnable
task gives you a Future
that does not provide a result but can be used to check if the task is complete or to cancel the task.
In summary, the choice between Runnable
and Callable
depends on whether you need to return a value from your task and handle exceptions. Callable
offers more flexibility and control, while Runnable
is a simpler option for tasks that do not return a result or need exception handling.
Runnable Example: Logging System
Imagine you have a multi-threaded application where various parts of the application need to log messages asynchronously. You don’t need to get any result back from the logging action; you just want the messages to be logged without blocking the main application flow.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestRunnable {
private static final int THREAD_POOL_SIZE = 5;
public static void main(String [] args){
ExecutorService executorService = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
executorTasks(executorService);
executorService.shutdown();
}
private static void executorTasks(ExecutorService executorService){
executorService.execute(new LogTask("Application Started."));
executorService.execute(new LogTask("Performing an action"));
executorService.execute(new LogTask("Application finished..."));
}
}
class LogTask implements Runnable{
private final String message;
LogTask(String message){
this.message = message;
}
@Override
public void run(){
Logger.log(message);
}
}
class Logger {
static void log(String message){
System.out.println("Logger:"+ message);
}
}
In this example, the LogTask
doesn’t need to return any result. It just acts (logging) and finishes. This ExecutorService
is used to manage a pool of threads that execute logging tasks concurrently.
Callable Example: Currency Converter
Consider a scenario where you’re building a financial application that needs to fetch the latest currency exchange rates from a remote API and perform conversions. You need the conversion result to proceed with transaction processing, and you also need to handle the possibility of exceptions (e.g., network issues).
import java.util.concurrent.*;
public class CurrencyConverter implements Callable<Double> {
private static final int SIMULATED_DELAY = 5000;
private final double amount;
private final String fromCurrency;
private final String toCurrency;
public CurrencyConverter(double amount, String fromCurrency, String toCurrency){
this.amount = amount;
this.fromCurrency = fromCurrency;
this.toCurrency = toCurrency;
}
@Override
public Double call() throws InterruptedException {
double exchangeRate = ExchangeRateService.fetchExchangeRate(fromCurrency, toCurrency);
return exchangeRate * amount;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();
Future<Double> futureResult = executorService.submit(new CurrencyConverter(100, "ERO", "USD"));
System.out.println("Main thread is now waiting for the currency conversion result...");
Double result = futureResult.get(); // This line makes the main thread wait until the callable task completes
System.out.println("Currency conversion completed. Main thread resumes.");
System.out.println("Converted amount: " + result);
executorService.shutdown();
}
}
class ExchangeRateService {
private static final int SIMULATED_DELAY = 5000;
static double fetchExchangeRate(String fromCurrency, String toCurrency) throws InterruptedException {
Thread.sleep(SIMULATED_DELAY); // Simulate a 5-second delay
return 1.1; // This should be replaced with actual code to fetch the exchange rate
}
}
import java.util.concurrent.*;
public class CurrencyConverter implements Callable<Double> {
private static final int SIMULATED_DELAY = 5000;
private final double amount;
private final String fromCurrency;
private final String toCurrency;
public CurrencyConverter(double amount, String fromCurrency, String toCurrency){
this.amount = amount;
this.fromCurrency = fromCurrency;
this.toCurrency = toCurrency;
}
@Override
public Double call() throws InterruptedException {
double exchangeRate = ExchangeRateService.fetchExchangeRate(fromCurrency, toCurrency);
return exchangeRate * amount;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();
Future<Double> futureResult = executorService.submit(new CurrencyConverter(100, "ERO", "USD"));
System.out.println("Main thread is now waiting for the currency conversion result...");
Double result = futureResult.get(); // This line makes the main thread wait until the callable task completes
System.out.println("Currency conversion completed. Main thread resumes.");
System.out.println("Converted amount: " + result);
executorService.shutdown();
}
}
class ExchangeRateService {
private static final int SIMULATED_DELAY = 5000;
static double fetchExchangeRate(String fromCurrency, String toCurrency) throws InterruptedException {
Thread.sleep(SIMULATED_DELAY); // Simulate a 5-second delay
return 1.1; // This should be replaced with actual code to fetch the exchange rate
}
}
Output
Main Thread is waiting.
Main Thread waiting is done.
Converted amount:110.00000000000001
In this Callable
example, the CurrencyConverter
needs to return the result of the currency conversion, which is crucial for the next steps in the application. The Future
object allows the main thread to wait for the result and retrieve it once available. Moreover, the Callable
interface enables the call
method to throw exceptions, which is important for error handling when dealing with network operations or other error-prone tasks.
Both examples demonstrate how Runnable
and Callable
can be used in real-world applications, depending on whether or not you need to return a result from your thread’s execution.
Runnable and Callable real-time examples
Runnable
for Fire-and-Forget Tasks in Java
When you have a task that you want to execute asynchronously without waiting for its result, Runnable
is a great choice. In this blog post, we will explore some creative approaches to use Runnable
for fire-and-forget tasks 🧨🎆.
1. Basic Fire-and-Forget
If you have a simple task that doesn’t return any value and doesn’t need to be tracked, use a Runnable
. Here’s an example:
Executor executor = Executors.newFixedThreadPool(5);
executor.execute(() -> {
// Your fire-and-forget task here
System.out.println("Fire-and-forget task executed!");
});
2. Logging or Metrics Collection
You can use a Runnable
to log events or collect metrics in the background. For instance, you can log user actions, API requests, or system events without blocking the main thread. Here’s how you can do it:
Executor executor = Executors.newSingleThreadExecutor();
executor.execute(() -> {
// Log user activity or collect metrics
logUserActivity("User clicked a button");
});
3. Cleanup or Resource Releasing
Release resources or perform cleanup tasks asynchronously. For example, you can close database connections, delete temporary files, or release memory. Here’s an example:
Executor executor = Executors.newCachedThreadPool();
executor.execute(() -> {
// Release resources (e.g., close database connections)
cleanupResources();
});
4. Sending Notifications
Use a Runnable
to send notifications (e.g., emails, SMS, push notifications) without blocking the main thread. Here’s how you can do it:
Executor executor = Executors.newFixedThreadPool(3);
executor.execute(() -> {
// Send notification to user
sendNotification("Your order has shipped!");
});
5. Background Cache Refresh
Refresh caches or update data in the background periodically. Use a Runnable
to schedule cache updates without affecting the main application flow. Here’s an example:
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
// Refresh cache data
refreshCache();
}, 0, 1, TimeUnit.HOURS);
Remember, the key idea is to offload non-blocking tasks to a separate thread using Runnable
. These tasks run independently, allowing your main application to continue without waiting for their completion. This approach can significantly improve the performance and responsiveness of your application.
Power of Callable: Real-World Examples for Asynchronous Tasks
Absolutely! Callable
is another interface provided by Java that is similar to Runnable, but it can return a result and throw an Exception. Here are some real-world usage examples:
1 . Data Processing
If you have a task that processes data and returns a result, use a Callable
.Example:
ExecutorService executor = Executors.newFixedThreadPool(5);
Future<Integer> future = executor.submit(() -> {
// Your data processing task here
int processedData = processData();
return processedData;
}
);
2. Web Scraping
Use a Callable
to scrape web pages and return the scraped data.Example:
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(() -> {
// Scrape a web page
String scrapedData = scrapeWebPage("https://example.com");
return scrapedData;
});
3. Database Operations
Perform database operations and return the result. Example:
ExecutorService executor = Executors.newCachedThreadPool();
Future<List<User>> future = executor.submit(() -> {
// Query database
List<User> users = queryDatabase("SELECT * FROM users");
return users;
});
4. File Operations
Read or write files and return the result.
Example:
ExecutorService executor = Executors.newFixedThreadPool(3);
Future<String> future = executor.submit(() -> {
// Read a file
String content = readFile("/path/to/file");
return content;
});
5. Scheduled Tasks
Schedule tasks that return a result.Example:
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
ScheduledFuture<String> future = scheduler.schedule(() -> { // A task that returns a result
String result = performTask();
return result;
}, 1, TimeUnit.HOURS);
Remember, the key idea is to offload tasks that return a result to a separate thread using Callable
. These tasks run independently, allowing your main application to continue without waiting for their completion. However, you can get the result of the computation when needed using the Future
object. 🚀