Callable & Future
When you need a background task to return a value or throw a checked exception, Runnable falls short. That’s exactly why Java introduced Callable<V> and Future<V> — a clean pair of interfaces that give your threads a voice to report back results (or errors) to the caller.
What Are Callable and Future?
| Interface | Package | Key Method | Returns | Can throw? |
|---|---|---|---|---|
Runnable | java.lang | run() | void | No checked exceptions |
Callable<V> | java.util.concurrent | call() | V | Yes — any checked exception |
Future<V> | java.util.concurrent | get() | V | Wraps exception in ExecutionException |
Think of Callable as a smarter Runnable, and Future as a receipt you hold while the task runs elsewhere — you can redeem it later for the result.
Callable in Action
Callable<V> is a functional interface with one method: V call() throws Exception.
import java.util.concurrent.Callable;
public class SumCallable implements Callable<Integer> {
private final int limit;
public SumCallable(int limit) {
this.limit = limit;
}
@Override
public Integer call() {
int sum = 0;
for (int i = 1; i <= limit; i++) {
sum += i;
}
return sum;
}
}
You can’t run a Callable with new Thread() — you submit it to an ExecutorService.
Submitting to an ExecutorService
import java.util.concurrent.*;
public class CallableDemo {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newSingleThreadExecutor();
Callable<Integer> task = new SumCallable(100);
Future<Integer> future = executor.submit(task); // returns immediately
System.out.println("Task submitted. Doing other work...");
int result = future.get(); // blocks until result is ready
System.out.println("Sum = " + result);
executor.shutdown();
}
}
Output:
Task submitted. Doing other work...
Sum = 5050
executor.submit() returns a Future<V> right away. Your main thread continues until it actually needs the result — at which point future.get() blocks if the task isn’t done yet.
Using Lambdas with Callable
Because Callable is a functional interface, you can use a lambda expression to keep things concise:
import java.util.concurrent.*;
public class LambdaCallable {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(2);
Future<String> future = executor.submit(() -> {
Thread.sleep(500); // simulate work
return "Hello from a Callable!";
});
System.out.println("Waiting for result...");
System.out.println(future.get());
executor.shutdown();
}
}
Output:
Waiting for result...
Hello from a Callable!
Future Methods
Future<V> gives you fine-grained control over the task:
| Method | Description |
|---|---|
get() | Blocks indefinitely until result is available |
get(long timeout, TimeUnit unit) | Blocks for at most the given time |
isDone() | Returns true if task completed (normally, cancelled, or with exception) |
isCancelled() | Returns true if task was cancelled before completing |
cancel(boolean mayInterruptIfRunning) | Attempts to cancel the task |
get() with a Timeout
Waiting forever can freeze your application. Always prefer get(timeout, unit) in production:
import java.util.concurrent.*;
public class FutureTimeout {
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(() -> {
Thread.sleep(3000); // takes 3 seconds
return "Slow result";
});
try {
String result = future.get(1, TimeUnit.SECONDS); // only wait 1s
System.out.println(result);
} catch (TimeoutException e) {
System.out.println("Task took too long! Cancelling...");
future.cancel(true);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} finally {
executor.shutdown();
}
}
}
Output:
Task took too long! Cancelling...
Warning: If your
Callablethrows an exception,future.get()wraps it in anExecutionException. Always calle.getCause()to get the original exception.
Handling Exceptions from Callable
import java.util.concurrent.*;
public class CallableException {
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(() -> {
if (true) throw new ArithmeticException("Division by zero!");
return 42;
});
try {
int result = future.get();
} catch (ExecutionException e) {
System.out.println("Task failed: " + e.getCause().getMessage());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
executor.shutdown();
}
}
}
Output:
Task failed: Division by zero!
Running Multiple Callables
When you have many tasks, invokeAll() submits them all and returns a list of Futures once all complete. invokeAny() returns the result of the first successful task.
import java.util.concurrent.*;
import java.util.*;
public class MultipleCallables {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(3);
List<Callable<String>> tasks = List.of(
() -> "Result from task 1",
() -> "Result from task 2",
() -> "Result from task 3"
);
List<Future<String>> futures = executor.invokeAll(tasks);
for (Future<String> f : futures) {
System.out.println(f.get());
}
executor.shutdown();
}
}
Output:
Result from task 1
Result from task 2
Result from task 3
Tip: Use
invokeAny()for race-condition patterns — for example, querying multiple services and taking whoever responds first.
CompletableFuture: The Modern Alternative
Java 8 introduced CompletableFuture<T>, which extends Future with non-blocking callbacks, chaining, and combining. It’s the preferred choice for new asynchronous code:
import java.util.concurrent.CompletableFuture;
public class CompletableFutureDemo {
public static void main(String[] args) throws Exception {
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> "Hello") // runs async
.thenApply(s -> s + ", World!") // transforms result
.thenApply(String::toUpperCase); // chains another step
System.out.println(future.get());
}
}
Output:
HELLO, WORLD!
Note:
CompletableFuturedoesn’t require anExecutorServicedirectly — it defaults to the commonForkJoinPool. For production workloads, pass your own executor tosupplyAsync(task, executor).
Under the Hood
When you call executor.submit(callable), the ExecutorService wraps your Callable inside a FutureTask<V> — which implements both Runnable (so it can be handed to a worker thread) and Future<V> (so you can query the result).
Internally, FutureTask uses a state machine with states like NEW, COMPLETING, NORMAL, EXCEPTIONAL, and CANCELLED. The result is stored in a field protected by volatile semantics and a happens-before relationship (see Java Memory Model), so a thread calling get() safely sees the value written by the worker thread.
get() blocks using LockSupport.park(), which suspends the calling thread without spinning. When the task finishes, the worker calls LockSupport.unpark() on all waiting threads — making it CPU-efficient while you wait.
For virtual threads (Project Loom, Java 21), Callable/Future work exactly the same way — you simply submit to a virtual-thread executor (Executors.newVirtualThreadPerTaskExecutor()), and blocking on future.get() no longer blocks an OS thread.
Quick Comparison
| Feature | Runnable | Callable<V> |
|---|---|---|
| Return value | No | Yes (V) |
| Checked exceptions | No | Yes |
Can be used with Thread | Yes | No (needs ExecutorService) |
| Can be used as lambda | Yes | Yes |
| Available since | Java 1.0 | Java 5 |
Related Topics
- Thread Pools & Executors — the
ExecutorServicethat runs yourCallabletasks - Multithreading — the fundamentals of concurrent execution in Java
- Java Memory Model — why
volatileand happens-before matter forFuturecorrectness - Virtual Threads (Project Loom) — Java 21’s lightweight threads that make blocking on
Future.get()cheap - Synchronization — coordinating shared state when multiple tasks access the same data
- Lambda Expressions — write
Callabletasks as concise one-liners