Java provides robust multithreading capabilities through the java.lang
and java.util.concurrent
packages. Two essential interfaces for creating tasks that can run concurrently are Runnable
and Callable
. While both are used to encapsulate tasks meant to run in a separate thread, they have key differences in how they are implemented and what they return.
1. Runnable Interface: Overview
The Runnable
interface has been part of Java since version 1.0. It represents a task that can be executed by a thread. It does not return any result and cannot throw a checked exception.
public interface Runnable {
void run();
}
Example of Runnable
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Task executed by: " + Thread.currentThread().getName());
}
}
public class RunnableExample {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}
- Does not return any result.
- Cannot throw checked exceptions.
- Used with
Thread
orExecutorService
.
2. Callable Interface: Overview
The Callable
interface was introduced in Java 5 as part of the java.util.concurrent
package. It is designed to return a result and may throw a checked exception.
public interface Callable<V> {
V call() throws Exception;
}
Example of Callable
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "Task executed by: " + Thread.currentThread().getName();
}
}
public class CallableExample {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(new MyCallable());
System.out.println(future.get()); // Waits and retrieves the result
executor.shutdown();
}
}
- Returns a result of a generic type.
- Can throw checked exceptions.
- Must be used with
ExecutorService
andFuture
.
3. Runnable vs Callable: Detailed Comparison
Feature | Runnable | Callable |
---|---|---|
Return Type | void | Generic type (e.g., String , Integer ) |
Throws Exceptions | Cannot throw checked exceptions | Can throw checked exceptions |
Used With | Thread or ExecutorService |
Only with ExecutorService |
Available Since | Java 1.0 | Java 5 |
4. When Should You Use Runnable?
Use Runnable
when:
- You don’t need to return a result from the task.
- Your code is simple and doesn’t throw checked exceptions.
- You are using traditional threads or basic task execution.
Example: Using Runnable with ExecutorService
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(() -> System.out.println("Runnable task running"));
executor.shutdown();
5. When Should You Use Callable?
Use Callable
when:
- You need the task to return a result.
- You need to handle checked exceptions.
- You are using concurrent task execution with
ExecutorService
.
Example: Callable with Multiple Threads
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.*;
public class MultipleCallableExample {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService executor = Executors.newFixedThreadPool(3);
List<Callable<String>> tasks = Arrays.asList(
() -> "Task 1 result",
() -> "Task 2 result",
() -> "Task 3 result"
);
List<Future<String>> futures = executor.invokeAll(tasks);
for (Future<String> future : futures) {
System.out.println(future.get());
}
executor.shutdown();
}
}
6. Handling Exceptions in Callable
The call()
method in Callable allows us to throw exceptions, making it a better choice for tasks where error handling is important.
class ErrorProneCallable implements Callable<Integer> {
public Integer call() throws Exception {
throw new Exception("Simulated error");
}
}
7. Using Lambda with Runnable and Callable
// Runnable with Lambda
Runnable r = () -> System.out.println("Runnable Lambda");
// Callable with Lambda
Callable<String> c = () -> "Callable Lambda";
8. Real-World Use Case: Runnable vs Callable
Example: Logging Service (Runnable)
Use Runnable
when you’re just saving logs in a background thread, no return needed:
executor.execute(() -> logToFile("User logged in at " + LocalDateTime.now()));
Example: Data Fetching Service (Callable)
Use Callable
when fetching something from a database:
Callable<User> fetchUser = () -> db.getUserById("123");
Future<User> userFuture = executor.submit(fetchUser);
User user = userFuture.get();