Future
Future<V> is a Java interface that represents the result of an asynchronous computation — a value that may not yet be available but will be at some point in the future. A Future is produced by submitting a Callable to an ExecutorService, or by wrapping a Callable in a FutureTask. The interface provides five methods: get() blocks until the result is available, get(timeout, unit) blocks for at most a specified duration, cancel(mayInterruptIfRunning) attempts to cancel the computation, isCancelled() tests cancellation, and isDone() tests completion (which includes both successful completion and cancellation or exception). Future's strength is its simplicity — it provides a clean handle for tracking and retrieving a deferred result. Its limitation is its blocking nature: get() always blocks the calling thread, making it unsuitable for non-blocking asynchronous pipelines. This entry covers the complete Future API and the precise semantics of each method, the ExecutionException wrapping, the interaction between cancel() and thread interruption, the polling pattern with isDone() and its pitfalls, FutureTask as the concrete implementation underlying ExecutorService.submit(), and the practical failure modes of Future-based code.
The Future API — get(), cancel(), isDone(), and isCancelled()
// ── Basic Future lifecycle ────────────────────────────────────────────
ExecutorService exec = Executors.newFixedThreadPool(4);
// Normal completion:
Future<Integer> normalFuture = exec.submit(() -> {
Thread.sleep(200);
return 42;
});
System.out.println("isDone before: " + normalFuture.isDone()); // false
Integer value = normalFuture.get(); // blocks ~200ms
System.out.println("isDone after: " + normalFuture.isDone()); // true
System.out.println("Result: " + value); // 42
// ── ExecutionException — when the task throws ────────────────────────
Future<String> failingFuture = exec.submit(() -> {
throw new IOException("Connection refused");
});
try {
failingFuture.get();
} catch (ExecutionException e) {
Throwable cause = e.getCause(); // the original IOException
System.out.println("Task threw: " + cause.getClass().getSimpleName()); // IOException
System.out.println("Message: " + cause.getMessage()); // Connection refused
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// ── get() with timeout ────────────────────────────────────────────────
Future<String> slowFuture = exec.submit(() -> {
Thread.sleep(5000);
return "slow result";
});
try {
String result = slowFuture.get(1, TimeUnit.SECONDS); // wait at most 1s
System.out.println("Got: " + result);
} catch (TimeoutException e) {
System.out.println("Timed out — task still running: " + !slowFuture.isDone()); // true
// Task is still running! Must explicitly cancel if no longer needed:
boolean cancelled = slowFuture.cancel(true); // interrupt the running thread
System.out.println("Cancelled: " + cancelled); // true
System.out.println("isCancelled: " + slowFuture.isCancelled()); // true
} catch (ExecutionException | InterruptedException e) {
Thread.currentThread().interrupt();
}
// ── cancel() semantics in detail ──────────────────────────────────────
// Case 1: cancel before task starts
Future<?> notStarted = exec.submit(() -> { Thread.sleep(10_000); return null; });
// Briefly let pool fill with other tasks so this one stays queued
boolean cancelledBeforeStart = notStarted.cancel(false);
System.out.println("Cancelled before start: " + cancelledBeforeStart); // true
System.out.println("Task prevented from running: " + notStarted.isCancelled()); // true
// Case 2: cancel(true) while running — interrupts the thread
Future<?> interruptible = exec.submit(() -> {
try {
Thread.sleep(10_000); // interruptible — throws InterruptedException on interrupt
} catch (InterruptedException e) {
System.out.println("Task was interrupted — cleaning up");
Thread.currentThread().interrupt(); // restore flag before returning
}
return null;
});
Thread.sleep(100); // let it start
interruptible.cancel(true); // interrupts the sleeping thread
// Case 3: cancel(false) while running — marks cancelled but doesn't interrupt
Future<?> uninterruptible = exec.submit(() -> {
long end = System.currentTimeMillis() + 500;
while (System.currentTimeMillis() < end) {
// busy loop — not checking interrupt flag, not calling interruptible methods
}
return null; // this WILL complete despite cancel(false)
});
Thread.sleep(100);
uninterruptible.cancel(false); // task keeps running
System.out.println("Running despite cancel: " + !uninterruptible.isDone()); // likely true
exec.shutdown();FutureTask, Polling Patterns, and Collecting Multiple Futures
// ── FutureTask — direct use ──────────────────────────────────────────
FutureTask<String> task = new FutureTask<>(() -> {
Thread.sleep(300);
return "computed value";
});
// Run in a raw Thread (no pool needed):
Thread t = new Thread(task, "future-task-runner");
t.start();
System.out.println("Task done: " + task.isDone()); // false (still sleeping)
String result = task.get(); // blocks until done
System.out.println("Result: " + result); // computed value
// ── FutureTask with done() callback ──────────────────────────────────
class CallbackFuture<V> extends FutureTask<V> {
private final Consumer<V> onSuccess;
private final Consumer<Throwable> onFailure;
CallbackFuture(Callable<V> callable, Consumer<V> onSuccess, Consumer<Throwable> onFailure) {
super(callable);
this.onSuccess = onSuccess;
this.onFailure = onFailure;
}
@Override
protected void done() { // called on ANY terminal transition
if (!isCancelled()) {
try {
onSuccess.accept(get()); // get() never blocks here — already done
} catch (ExecutionException e) {
onFailure.accept(e.getCause());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
ExecutorService exec = Executors.newFixedThreadPool(2);
CallbackFuture<Integer> cbFuture = new CallbackFuture<>(
() -> 21 * 2,
v -> System.out.println("Success: " + v), // called when done
ex -> System.err.println("Failure: " + ex) // called on exception
);
exec.execute(cbFuture);
Thread.sleep(100); // let it complete and trigger done()
// ── Naive multi-future collection — suboptimal ────────────────────────
List<Future<Long>> futures = new ArrayList<>();
for (int i = 0; i < 5; i++) {
final int duration = (5 - i) * 100; // 500ms, 400ms, 300ms, 200ms, 100ms
futures.add(exec.submit(() -> { Thread.sleep(duration); return (long) duration; }));
}
// WRONG: processes in submission order — blocks on slowest first
for (Future<Long> f : futures) {
System.out.println("Got (slow order): " + f.get()); // 500, 400, 300, 200, 100
}
// BETTER: invokeAll blocks until all done, then get() never blocks:
List<Future<Long>> allDone = exec.invokeAll(List.of(
() -> { Thread.sleep(500); return 500L; },
() -> { Thread.sleep(100); return 100L; },
() -> { Thread.sleep(300); return 300L; }
));
for (Future<Long> f : allDone) {
System.out.println("All done: " + f.get()); // no blocking — all already complete
}
// ── ExecutorCompletionService — process futures as they complete ──────
ExecutorCompletionService<String> completion = new ExecutorCompletionService<>(exec);
completion.submit(() -> { Thread.sleep(300); return "slow"; });
completion.submit(() -> { Thread.sleep(50); return "fast"; });
completion.submit(() -> { Thread.sleep(150); return "medium"; });
// take() returns the NEXT completed future — in completion order, not submission order:
for (int i = 0; i < 3; i++) {
Future<String> done = completion.take(); // blocks until one completes
System.out.println("Completed: " + done.get()); // fast, medium, slow
}
// Output: fast, medium, slow — processed immediately as each completes
// poll() non-blocking variant — returns null if none complete yet:
Future<String> maybeReady = completion.poll(100, TimeUnit.MILLISECONDS);
System.out.println("Polled: " + (maybeReady != null ? maybeReady.get() : "nothing ready"));
exec.shutdown();Future Limitations and When to Use CompletableFuture Instead
// ── Limitation 1: blocking — no callback support ─────────────────────
ExecutorService exec = Executors.newFixedThreadPool(4);
// Future: must block to react to completion:
Future<String> future = exec.submit(() -> fetchFromNetwork());
String result = future.get(); // BLOCKS calling thread — the thread is idle waiting
processResult(result); // only runs after block completes
// CompletableFuture: callback, no blocking:
CompletableFuture.supplyAsync(() -> fetchFromNetwork(), exec)
.thenAccept(r -> processResult(r)); // runs when complete — no thread blocked
// Calling thread continues immediately
// ── Limitation 2: no composition ──────────────────────────────────────
// Future: two sequential async ops requires blocking in between:
Future<String> step1 = exec.submit(() -> fetchUserId());
String userId = step1.get(); // BLOCKS for step1
Future<User> step2 = exec.submit(() -> fetchUser(userId));
User user = step2.get(); // BLOCKS for step2 — two blocking threads wasted
// CompletableFuture: non-blocking chain:
CompletableFuture.supplyAsync(() -> fetchUserId(), exec)
.thenComposeAsync(id -> CompletableFuture.supplyAsync(() -> fetchUser(id), exec))
.thenAccept(user -> renderProfile(user));
// No threads blocked — each step starts only when the previous completes
// ── Limitation 3: exception handling requires get() ───────────────────
// Future: must block to see exceptions:
Future<String> risky = exec.submit(() -> { throw new RuntimeException("fail"); });
try {
risky.get(); // BLOCKS, then throws ExecutionException
} catch (ExecutionException e) {
String recovered = "default"; // recovery is here, after the block
useResult(recovered);
}
// CompletableFuture: non-blocking exception handling:
CompletableFuture.supplyAsync(() -> { throw new RuntimeException("fail"); }, exec)
.exceptionally(ex -> "default") // non-blocking recovery on exception
.thenAccept(r -> useResult(r)); // always called with either real result or "default"
// ── When Future IS the right choice ──────────────────────────────────
// Simple test — just block and assert:
Future<Integer> testFuture = exec.submit(() -> compute());
assertEquals(42, testFuture.get(5, TimeUnit.SECONDS));
// Batch processing with invokeAll — all futures done before any get():
List<Callable<Report>> reportTasks = buildReportTasks();
List<Future<Report>> reportFutures = exec.invokeAll(reportTasks);
List<Report> reports = reportFutures.stream()
.map(f -> { try { return f.get(); } catch (Exception e) { throw new RuntimeException(e); } })
.collect(toList());
// No wasted blocking — invokeAll ensures all are done before we call get()
// First-success pattern with invokeAny — no manual Future management:
String firstResponse = exec.invokeAny(List.of(
() -> callPrimary(),
() -> callSecondary(),
() -> callTertiary()
));
// invokeAny handles cancelling losers — cleaner than manual Future.cancel()
exec.shutdown();