Blog Concurrency and Multithreading Java

Top 6 Difference Between Callable and Runnable in Java: A Detailed Comparison and Guide🧑‍🏫

Difference Between Callable and Runnable

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 Exceptionvoid 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🎯.

Java
@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 the run() 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 the run() method itself.

    Let’s go through an example to illustrate how to handle exceptions within a Runnable task.

    Example Scenario: Reading a File with Runnable
    Imagine you have a task to read content from a file and print it to the console. Reading a file can throw a FileNotFoundException (if the file does not exist) or an IOException (if an I/O error occurs during reading). Both of these exceptions are checked exceptions, so they must be handled within the run() 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 method call() that you need to implement. Unlike run(), the call() method can return a value and throw checked exceptions.
Java
@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.

Java

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).

Java
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

Java
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:

Java
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:

Java
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:

Java
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:

Java
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:

Java
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:

Java
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:

Java
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:

Java
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:

Java
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:

Java
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. 🚀

Avatar

Neelabh

About Author

As Neelabh Singh, I am a Senior Software Engineer with 6.6 years of experience, specializing in Java technologies, Microservices, AWS, Algorithms, and Data Structures. I am also a technology blogger and an active participant in several online coding communities.

You may also like

Blog Design Pattern

Understanding the Builder Design Pattern in Java | Creational Design Patterns | CodeTechSummit

Overview The Builder design pattern is a creational pattern used to construct a complex object step by step. It separates
Blog Tech Toolkit

Base64 Decode

Base64 encoding is a technique used to encode binary data into ASCII characters, making it easier to transmit data over