Runnable & Callable
Runnable & Callable
Runnable and Callable<V> both represent a unit of work handed to a thread or an executor, but they differ in two ways that matter constantly in real code: Runnable.run() returns nothing and cannot throw a checked exception, while Callable<V>.call() returns a value of type V and can throw a checked Exception directly.
What Is the Difference Between Runnable and Callable?
Runnable has existed since Java's very first release and models a task with no result and no checked exception. Callable<V> arrived later, in Java 5, as part of the same java.util.concurrent package that introduced ExecutorService and Future — specifically to give a task somewhere to put a return value and a legitimate way to fail with a checked exception.
Why Thread Safety Matters
Runnable and Callable are how work actually gets onto a thread in the first place — every synchronization problem covered later in this section starts with a task like these, running concurrently with something else. Getting the plumbing wrong here, before concurrency even becomes the issue, is what causes results to go missing or exceptions to disappear silently.
A Future that is never checked is a common way for a failed background task to fail completely silently — submit() returns immediately whether the task eventually succeeds or throws, and nothing surfaces that failure until something actually calls get().
Getting a result out of a Runnable means smuggling it through a shared, mutable field, since run() itself cannot return anything.
1// File: BeforeCallable.java
2
3public class BeforeCallable {
4
5 static class ResultHolder {
6 int value;
7 }
8
9 public static void main(String[] args) throws InterruptedException {
10 ResultHolder holder = new ResultHolder();
11
12 Runnable task = () -> holder.value = 21 * 2;
13
14 Thread thread = new Thread(task);
15 thread.start();
16 thread.join();
17
18 System.out.println("Result: " + holder.value);
19 }
20}Output:
Result: 42
Callable<V> returns its result directly, and ExecutorService.submit() hands that result back through a Future<V>.
1// File: AfterCallable.java
2import java.util.concurrent.*;
3
4public class AfterCallable {
5 public static void main(String[] args) throws Exception {
6 Callable<Integer> task = () -> 21 * 2;
7
8 try (ExecutorService executor = Executors.newSingleThreadExecutor()) {
9 Future<Integer> future = executor.submit(task);
10 System.out.println("Result: " + future.get());
11 }
12 }
13}Output:
Result: 42
No shared mutable field, no manual join()-then-read dance — future.get() returns the value directly, and blocks until it is ready.
How It Works
One prose sentence introducing the diagram: both interfaces flow through the same ExecutorService, but only one side of this diagram has a value at the end of it.
Runnable task Callable<V> task
| |
v v
ExecutorService.submit(...) ExecutorService.submit(...)
| |
v v
Future<?> Future<V>
| |
get() returns null get() returns the computed value V
call() can throw a checked Exception directly, something run() cannot do at all. When a submitted task throws, Future.get() wraps it in ExecutionException, with the original exception available through getCause().
1// File: CallableExceptionExample.java
2import java.util.concurrent.*;
3
4public class CallableExceptionExample {
5 public static void main(String[] args) throws InterruptedException {
6 Callable<Integer> risky = () -> {
7 throw new IllegalStateException("Lookup failed");
8 };
9
10 try (ExecutorService executor = Executors.newSingleThreadExecutor()) {
11 Future<Integer> future = executor.submit(risky);
12 try {
13 future.get();
14 } catch (ExecutionException e) {
15 System.out.println("Caused by: " + e.getCause().getClass().getSimpleName());
16 }
17 }
18 }
19}Output:
Caused by: IllegalStateException
Code Examples
Submitting a batch of independent Callable tasks at once, using ExecutorService.invokeAll(), runs every task concurrently and returns their results in the same order the tasks were submitted, regardless of which finishes first.
1// File: InvokeAllExample.java
2import java.util.*;
3import java.util.concurrent.*;
4
5public class InvokeAllExample {
6 public static void main(String[] args) throws Exception {
7 List<Callable<Integer>> tasks = List.of(
8 () -> 10 * 10,
9 () -> 20 * 20,
10 () -> 30 * 30
11 );
12
13 try (ExecutorService executor = Executors.newFixedThreadPool(3)) {
14 List<Future<Integer>> results = executor.invokeAll(tasks);
15 int total = 0;
16 for (Future<Integer> result : results) {
17 total += result.get();
18 }
19 System.out.println("Total: " + total);
20 }
21 }
22}Output:
Total: 1400
Fire-and-forget background work with no result needed, such as logging or a notification send, is exactly what a plain Runnable passed directly to new Thread(...) is for, without needing the full ExecutorService machinery at all.
Real-World Example
A train ticketing feature checks seat availability across three coach types at once, submitting each lookup as a Callable<Integer> so all three run concurrently instead of one after another.
1// File: SeatAvailabilityChecker.java
2import java.util.concurrent.Callable;
3
4public class SeatAvailabilityChecker implements Callable<Integer> {
5 private final String trainNumber;
6 private final String coachType;
7
8 public SeatAvailabilityChecker(String trainNumber, String coachType) {
9 this.trainNumber = trainNumber;
10 this.coachType = coachType;
11 }
12
13 @Override
14 public Integer call() {
15 return switch (coachType) {
16 case "SL" -> 42;
17 case "3A" -> 12;
18 case "2A" -> 4;
19 default -> 0;
20 };
21 }
22}1// File: SeatAvailabilityDemo.java
2import java.util.*;
3import java.util.concurrent.*;
4
5public class SeatAvailabilityDemo {
6 public static void main(String[] args) throws Exception {
7 List<Callable<Integer>> checks = List.of(
8 new SeatAvailabilityChecker("12951", "SL"),
9 new SeatAvailabilityChecker("12951", "3A"),
10 new SeatAvailabilityChecker("12951", "2A")
11 );
12
13 try (ExecutorService executor = Executors.newFixedThreadPool(3)) {
14 List<Future<Integer>> results = executor.invokeAll(checks);
15
16 int totalAvailable = 0;
17 for (Future<Integer> result : results) {
18 totalAvailable += result.get();
19 }
20
21 System.out.println("Sleeper available: " + results.get(0).get());
22 System.out.println("AC 3-tier available: " + results.get(1).get());
23 System.out.println("AC 2-tier available: " + results.get(2).get());
24 System.out.println("Total available: " + totalAvailable);
25 }
26 }
27}Output:
Sleeper available: 42
AC 3-tier available: 12
AC 2-tier available: 4
Total available: 58
A mistake that appears often in fresher pull requests is running each availability lookup sequentially in a simple loop, one coach type after another, when the three checks have no dependency on each other at all. Submitting them as Callable tasks to invokeAll(), exactly as SeatAvailabilityDemo does here, lets all three run concurrently and still guarantees the results come back in the same order the tasks were submitted, so results.get(0) always corresponds to checks.get(0).
Best Practices
Use Runnable for work whose only purpose is a side effect, and Callable<V> the moment a result actually needs to come back to the caller.
Prefer invokeAll() over submitting a batch of Callable tasks individually in a loop when every result is needed together — it keeps the code simpler and preserves result order automatically.
Always unwrap ExecutionException.getCause() when handling a failed task's result, rather than treating the wrapper exception itself as the actual failure.
Keep a Callable's call() method free of unrelated side effects beyond producing its result — a task that both mutates shared state and returns a value is harder to reason about under concurrency.
Common Mistakes
Passing a Callable directly to Thread's constructor does not compile — Thread only accepts a Runnable.
1// This does not compile - Thread's constructor accepts Runnable, not Callable
2Callable<Integer> task = () -> 42;
3Thread thread = new Thread(task);Trying to throw a checked exception from inside a Runnable's body does not compile either, since run()'s signature declares no checked exceptions at all.
1// This does not compile - run() cannot declare or propagate a checked exception
2Runnable task = () -> {
3 throw new java.io.IOException("fails to compile");
4};Interview Questions
Q1. What is the difference between Runnable and Callable?
Runnable.run() returns void and cannot throw a checked exception. Callable<V>.call() returns a value of type V and can throw a checked Exception directly — Callable was added later, in Java 5, specifically to address both limitations. Service-company interviewers usually stop at this definition; product companies push further into what happens when a Callable actually fails.
Q2. Can Runnable's run() method throw a checked exception?
No. Its signature declares none, so any checked exception thrown inside a Runnable's body must be caught and handled within the lambda or method itself — it cannot propagate out as a checked exception, exactly as demonstrated in this article's Common Mistakes section.
Q3. What does ExecutorService.submit() return when given a Runnable vs a Callable?
Both return a Future. For a Runnable, it is Future<?>, and get() returns null on successful completion since there is no result. For a Callable<V>, it is Future<V>, and get() returns the task's actual computed value. The nuance interviewers are listening for is whether you know get() still blocks and can still throw for a Runnable's Future<?>, even though there is no result to retrieve.
Q4. What exception does Future.get() throw if the task itself threw an exception, and how do you access the original exception?
It throws ExecutionException, wrapping whatever the task actually threw. The original exception is retrieved through ExecutionException.getCause(), exactly as demonstrated in this article's CallableExceptionExample.
Q5. What is the difference between invokeAll() and submitting each Callable individually with submit()?
invokeAll() submits an entire collection of tasks at once and blocks until all of them complete, returning a List<Future<V>> in the same order the tasks were given. Calling submit() individually in a loop achieves a similar effect but requires manually collecting each returned Future and tracking the order yourself.
Q6. Can a Callable be passed directly to a Thread constructor?
No, Thread's constructor only accepts a Runnable — a Callable must go through an ExecutorService's submit() or invokeAll() methods, both of which accept it directly.
Q7. Does Future.get() block the calling thread?
Yes, the no-argument get() blocks until the task completes, whether successfully or with an exception. An overload accepting a timeout is available for cases where waiting indefinitely is not acceptable. This is a favorite product-company follow-up: what should happen in a request-handling thread that cannot afford to block forever waiting on a slow task.
FAQs
Can Runnable be used with ExecutorService, or is it only for raw Thread objects?
Both work. ExecutorService.submit(Runnable) and execute(Runnable) are both valid — Runnable is not limited to raw Thread objects at all.
What does Future<?>.get() return for a submitted Runnable?
null, once the task completes successfully — there is no result value to return, since Runnable.run() itself returns void.
Is Callable a functional interface, usable with a lambda?
Yes, Callable<V> has exactly one abstract method, call(), making it a valid functional interface usable with a lambda expression or method reference exactly like Runnable.
What happens if invokeAll() is called with an empty list of tasks?
It returns an empty List<Future<V>> immediately, with no error and nothing to wait for.
Can a Future be cancelled?
Yes, Future.cancel(boolean mayInterruptIfRunning) attempts to cancel the task — if it has not yet started, it will not run at all; if it is already running, the boolean argument controls whether the executing thread is interrupted.
Does the order of Future objects returned by invokeAll() match the order of submitted tasks?
Yes, this is a guaranteed part of the contract — the returned List<Future<V>> corresponds positionally to the input collection of tasks, regardless of which task actually finishes first.
Which Java version introduced Callable and the concurrency utilities package?
Java 5, in 2004, via JSR 166 — the same release that introduced java.util.concurrent as a whole, including ExecutorService, Future, and the other coordination utilities covered throughout this section.
Summary
Runnable and Callable<V> both represent a unit of work, but only Callable can return a value or throw a checked exception directly — Future<V>, returned by ExecutorService.submit(), is what carries that result or exception back to the calling code, wrapping any thrown exception in ExecutionException along the way.
The habit worth carrying forward from this article's seat-availability example is reaching for invokeAll() the moment several independent Callable tasks all need to run together, rather than running them one after another in a simple loop for no reason beyond habit.
What to Read Next
Learn the stages a thread passes through, start to finish.