Question 27 · Section 9

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

Language versions: English Russian Ukrainian

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

  1. Use Callable — when you need a result or checked exception
  2. Always use timeout for get()get(5, TimeUnit.SECONDS) instead of get()
  3. CompletableFuture for non-blocking code — Java 8+
  4. invokeAny() — for mirror requests
  5. invokeAll() with timeout — for batch execution
  6. Check isInterrupted() — inside long Callable to support cancel()
  7. ExecutionException.getCause() — to get the real cause
  8. 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 of get()
  • 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]]