What are Callable and Future?
Callable and Future are two interfaces from java.util.concurrent that solve fundamental limitations of Runnable. If Runnable describes a task without a result (void run()), then...
Callable and Future are two interfaces from java.util.concurrent that solve fundamental limitations of Runnable. If Runnable describes a task without a result (void run()), then Callable describes a task with a result (V call()), and Future is a “promise” of that result, which can be obtained later.
Why this is needed: in real applications, most tasks return a result (DB response, HTTP response, computation result). Runnable can neither return a value nor throw a checked exception — making it unsuitable for many scenarios.
Junior Level
Basic Understanding
Callable and Future are interfaces from java.util.concurrent that solve Runnable’s limitations:
| Runnable Problem | Solution |
|---|---|
| Cannot return a result | Callable — returns result of type V |
| Cannot throw checked exception | Callable — method call() throws Exception |
| Cannot check task status | Future — status, result, cancellation |
Callable — like Runnable, but with a result. You define a task that computes something and returns it. Future — a “receipt” from ExecutorService: you submitted a Callable for execution and got a Future, which you can later use to pick up the result.
Callable
// Callable — like Runnable, but returns a result
Callable<String> task = () -> {
// Long operation
return "Result!";
};
// Runnable — no return
Runnable runnable = () -> {
System.out.println("No result");
};
Future
ExecutorService executor = Executors.newFixedThreadPool(2);
// submit returns a Future
Future<String> future = executor.submit(() -> {
Thread.sleep(2000);
return "Done!";
});
// Check status
System.out.println("Task done? " + future.isDone()); // false
// Get result (blocks until completion!)
String result = future.get(); // Wait 2 seconds...
System.out.println(result); // "Done!"
System.out.println("Task done? " + future.isDone()); // true
Simple Example
Callable<Integer> calculateTask = () -> {
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
return sum;
};
Future<Integer> future = executor.submit(calculateTask);
Integer result = future.get(); // Blocks until completion
System.out.println("Sum: " + result); // 5050
Middle Level
Callable vs Runnable
| Characteristic | Runnable | Callable |
|---|---|---|
| Method | void run() |
V call() throws Exception |
| Returns result | No | Yes |
| Checked Exception | No | Yes |
| Functional interface | Yes | Yes |
// Runnable:
Runnable r = () -> System.out.println("Hello");
// Callable:
Callable<String> c = () -> {
if (error) throw new IOException("Error!"); // Checked!
return "Success";
};
Future — Methods
| Method | Description |
|---|---|
get() |
Get result (blocks) |
get(timeout, unit) |
Get with timeout |
isDone() |
Task completed? |
isCancelled() |
Task cancelled? |
cancel(mayInterrupt) |
Cancel task |
Future with Timeout
Future<String> future = executor.submit(() -> {
Thread.sleep(10000); // Long task
return "Result";
});
try {
String result = future.get(3, TimeUnit.SECONDS);
System.out.println(result);
} catch (TimeoutException e) {
future.cancel(true); // Cancel the task
System.out.println("Task didn't finish in time");
}
invokeAll() — Wait for ALL Tasks
List<Callable<String>> tasks = List.of(
() -> "Task 1",
() -> "Task 2",
() -> "Task 3"
);
// Blocks until ALL tasks complete
List<Future<String>> futures = executor.invokeAll(tasks);
for (Future<String> f : futures) {
System.out.println(f.get()); // Task 1, Task 2, Task 3
}
invokeAny() — Wait for the FIRST Task
List<Callable<String>> tasks = List.of(
() -> { Thread.sleep(3000); return "Slow"; },
() -> { Thread.sleep(1000); return "Fast"; },
() -> { Thread.sleep(2000); return "Medium"; }
);
// Returns the result of the FIRST completed task
// Others are automatically cancelled
String result = executor.invokeAny(tasks); // "Fast"
Task Cancellation
Future<?> future = executor.submit(() -> {
for (int i = 0; i < 1_000_000; i++) {
// Important: check interruption!
if (Thread.currentThread().isInterrupted()) {
return; // Task cancelled
}
process(i);
}
});
// Cancel
boolean cancelled = future.cancel(true);
// true = send interrupt() to the thread
// But this does NOT guarantee stop — task must check isInterrupted()!
Senior Level
Under the Hood: FutureTask
When you call executor.submit(callable), the executor wraps your Callable in an internal FutureTask object:
// Simplified
public class FutureTask<V> implements RunnableFuture<V> {
private volatile int state; // NEW, COMPLETING, NORMAL, EXCEPTIONAL, CANCELLED
private Callable<V> callable;
private Object outcome; // Result or exception
public V get() throws InterruptedException, ExecutionException {
if (state < COMPLETING) {
// Blocks via AwaitNode (wait queue)
awaitDone(false, 0L);
}
return report(state);
}
}
FutureTask State Machine
| State | Description |
|---|---|
| NEW | Task created |
| COMPLETING | Result being written (transitional) |
| NORMAL | Successful completion |
| EXCEPTIONAL | Exception thrown |
| CANCELLED | Task cancelled (before launch) |
| INTERRUPTED | Task cancelled (interrupted) |
The Problem with Blocking get()
// get() BLOCKS the current thread
Future<String> future = executor.submit(callable);
String result = future.get(); // Thread is busy — cannot process other requests
// If you have 100 threads and each waits on future.get() → system "stops"
Solution: CompletableFuture (Java 8+):
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
return fetchData();
});
future.thenAccept(result -> {
// Callback — does NOT block!
System.out.println(result);
});
// Thread is free — can continue processing requests
ExecutionException — Unwrap the Cause
Future<Integer> future = executor.submit(() -> {
throw new IOException("Database connection failed");
});
try {
future.get();
} catch (ExecutionException e) {
// e — ExecutionException
// e.getCause() — IOException: Database connection failed
Throwable cause = e.getCause();
System.out.println(cause.getClass()); // java.io.IOException
System.out.println(cause.getMessage()); // Database connection failed
}
Lost Results
Future<String> future = executor.submit(() -> "Result");
future.cancel(true); // Cancelled
try {
future.get(); // → CancellationException
} catch (CancellationException e) {
System.out.println("Task was cancelled");
}
invokeAll() with Timeout
// ALWAYS use timeout!
List<Future<String>> futures = executor.invokeAll(tasks, 10, TimeUnit.SECONDS);
// If at least one task "hangs" — TimeoutException for all
Memory Overhead
Each FutureTask = object (~48 bytes)
Million tasks/sec = million objects → GC pressure
For virtual threads — better to use lighter mechanisms
Diagnostics
Without Blocking
Future<?> future = executor.submit(task);
// Check without blocking
if (future.isDone()) {
System.out.println("Completed");
}
if (future.isCancelled()) {
System.out.println("Cancelled");
}
ListenableFuture (Guava)
For older projects without Java 8+:
ListenableFuture<String> future = listeningExecutor.submit(callable);
Futures.addCallback(future, new FutureCallback<String>() {
public void onSuccess(String result) {
// Callback — doesn't block
}
public void onFailure(Throwable t) {
// Error handling
}
}, executor);
Best Practices
- Use Callable — when you need a result or checked exception
- Always use timeout for get() —
get(5, TimeUnit.SECONDS)instead ofget() - CompletableFuture for non-blocking code — Java 8+
- invokeAny() — for mirror requests
- invokeAll() with timeout — for batch execution
- Check isInterrupted() — inside long Callable to support cancel()
- ExecutionException.getCause() — to get the real cause
- Avoid millions of FutureTask — use CompletableFuture or VT
When NOT to Use Callable/Future
- Task without result (logging, sending metrics) — use Runnable, no point wrapping void in Callable
- Simple single-threaded computations — if parallel execution isn’t needed, Callable adds unnecessary complexity
- Need a non-blocking callback — Future.get() blocks the thread. Use CompletableFuture for async callback chains
- Java 21+ with Virtual Threads — for I/O-bound tasks it’s simpler to create a VT and return the result via closure than wrap in Callable/Future
- Very many short tasks (million/sec) — each FutureTask = object (~48 bytes), GC pressure. Use lock-free structures or batch processing
Callable/Future vs CompletableFuture vs Virtual Threads: What to Choose?
| Situation | Choice | Why |
|---|---|---|
| Task with result + blocking get | Callable + Future | Simple, clear |
| Task with result + timeout | Callable + Future.get(timeout) | Built-in support |
| Non-blocking callback | CompletableFuture | thenAccept, thenApply — without blocking |
| Many I/O-bound tasks (Java 21+) | Virtual Thread | Simple blocking code, scales |
| Mirror requests (whoever is faster) | invokeAny() or ShutdownOnSuccess | Auto-cancel slow ones |
| Batch execution | invokeAll() with timeout | Waits for all, time-limited |
Interview Cheat Sheet
Must know:
- Callable
returns a result (V call()) and can throw a checked exception; Runnable — void run(), no checked exception - Future — “receipt” from Executor: get() (blocks), isDone(), cancel(mayInterrupt)
- Always use timeout for get():
get(5, TimeUnit.SECONDS)instead ofget() - invokeAll() — waits for ALL tasks, invokeAny() — result of FIRST completed (others cancelled)
- ExecutionException.getCause() — unwrap the real error cause
- FutureTask states: NEW → COMPLETING → NORMAL/EXCEPTIONAL/CANCELLED
- CompletableFuture — non-blocking alternative (thenAccept, thenApply)
Frequent follow-up questions:
- Why doesn’t cancel(true) guarantee task stop? — cancel sends interrupt(), but the task must check isInterrupted() itself
- How is CompletableFuture better than Future? — Future.get() blocks the thread; CompletableFuture provides callback chains (thenAccept, thenApply) without blocking
- What is the overhead of FutureTask? — ~48 bytes per object; million tasks/sec = GC pressure
- **Why does invokeAll() return List
and not List ?** — Because each task can complete with an exception; result must be obtained via future.get() with ExecutionException handling
Red flags (DO NOT say):
- “Future.get() is an asynchronous operation” — get() blocks the calling thread until the task completes
- “cancel() instantly stops the task” — cancel only sends interrupt, the task must handle it
- “Callable is needed for tasks without result” — for tasks without result, use Runnable, Callable — when you need a result or checked exception
- “Future can be reused” — Future is one-time-use; for re-execution you need a new submit
Related topics:
- [[26. What is structured concurrency]]
- [[27. What is the difference between Thread and Runnable]]
- [[23. What are Virtual Threads in Java 21]]
- [[22. How to avoid race condition]]