☕ Java

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()

Future<V> has five methods, each with precise semantics that interact with the underlying task's lifecycle state. get() is the primary result-retrieval method. It blocks the calling thread indefinitely until the computation completes. If the computation completes normally, get() returns the result value. If the computation threw an exception, get() wraps that exception in an ExecutionException and throws it — the original exception is accessible via ExecutionException.getCause(). If the computation was cancelled before or during execution, get() throws CancellationException. If the calling thread is interrupted while blocked in get(), it throws InterruptedException and the calling thread's interrupt flag is cleared (consistent with the InterruptedException contract — the flag is cleared by throwing). get(long timeout, TimeUnit unit) adds a time bound to the blocking wait. If the result is not available within the timeout, it throws TimeoutException. The task continues running after TimeoutException — the timeout affects only the waiting thread, not the task itself. The caller must decide what to do after TimeoutException: cancel the task (future.cancel(true)), wait again, or proceed without the result. Crucially, TimeoutException does not automatically cancel the task; the caller must explicitly cancel if desired. cancel(boolean mayInterruptIfRunning) attempts to cancel the task. If the task has not yet started, cancel() prevents it from starting and returns true. If the task is already running and mayInterruptIfRunning is true, cancel() interrupts the task's executing thread (calls Thread.interrupt() on it) and returns true. If the task is already running and mayInterruptIfRunning is false, cancel() signals cancellation intent but does not interrupt the thread — the task continues running to completion, but isCancelled() returns true and get() throws CancellationException. If the task has already completed, cancel() returns false and has no effect. Note that cancel() with mayInterruptIfRunning=true does not stop the task — it interrupts the thread, which the task's code may or may not respond to depending on whether it checks the interrupt flag or calls interruptible methods. isDone() returns true when the computation has completed in any way: normal completion, exception, or cancellation. It never blocks. isCancelled() returns true specifically when the task was cancelled. A cancelled task is also done (isCancelled() implies isDone()), but a done task is not necessarily cancelled. These two methods together provide a complete picture of the task's terminal state. The state machine of Future has five states: NEW, COMPLETING, NORMAL, EXCEPTIONAL, CANCELLED, and INTERRUPTED (FutureTask adds INTERRUPTING as a transient state). From the caller's perspective, a Future is in one of three terminal states visible through the API: normal completion (isDone()=true, isCancelled()=false, get() returns value), exceptional completion (isDone()=true, isCancelled()=false, get() throws ExecutionException), or cancelled (isDone()=true, isCancelled()=true, get() throws CancellationException).
Java
// ── 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<V> is the concrete class that implements both Runnable and Future<V>. It is what ExecutorService.submit(Callable) returns internally (wrapped in a Future interface). FutureTask can be used directly when you need to execute a Callable via a raw Thread rather than a thread pool, or when you want to pre-build tasks and submit them to a pool later, or when you want to inspect or extend Future's behavior. FutureTask exposes a protected done() method that is called upon transition to any terminal state (normal completion, exception, or cancellation). Subclassing FutureTask and overriding done() enables callback behavior: notification when a task completes without the caller having to block in get(). This is a lightweight alternative to CompletableFuture for simple notification scenarios. The polling pattern — calling isDone() in a loop and only calling get() when isDone() returns true — avoids blocking but creates a busy-wait. If the loop runs without any sleep, it wastes CPU. If it sleeps between iterations, it introduces latency proportional to the sleep duration. Polling is rarely the right pattern; blocking get() or time-bounded get() with explicit timeout handling is usually cleaner. The one legitimate use of isDone() without immediately calling get() is checking whether a task has completed before deciding whether to cancel it — if isDone() is true, cancel() will have no effect and you can call get() directly. Collecting results from multiple Futures requires careful handling. The naive pattern — looping through a List<Future<T>> and calling get() on each in order — introduces latency: if future[0] takes 10 seconds and future[1] completes in 1 second, you wait 10 seconds before retrieving future[1]'s result, even though it has been available for 9 seconds. ExecutorService.invokeAll() handles this correctly: it blocks until all tasks complete and returns all futures in a done state, so subsequent get() calls on the returned futures never block. For the "return the first successful result" pattern, invokeAny() is the correct tool. For processing futures as they complete (rather than in submission order), Java 8 CompletableFuture and Java 9 CompletableFuture.allOf/anyOf are the modern alternatives; prior to CompletableFuture, ExecutorCompletionService was the solution. ExecutorCompletionService<V> wraps an ExecutorService and provides a take() method that returns completed futures in completion order — the first future to complete is the first returned from take(), regardless of submission order. This is critical for pipelines where you want to process results as they arrive rather than waiting for the slowest task before processing the fastest.
Java
// ── 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

Future has three fundamental limitations that motivated the design of CompletableFuture in Java 8. Understanding these limitations precisely helps you recognize when Future is sufficient and when you need CompletableFuture. The first limitation is that Future's get() always blocks. There is no way to say "when this future completes, run this callback" without blocking a thread to wait for it. If you have ten asynchronous operations and want to chain a callback after each, you must block ten threads waiting for ten futures, even if the actual computation is I/O-bound and those threads spend almost all their time blocked. This makes Future-based code inefficient under high concurrency and forces synchronous interaction with what should be asynchronous operations. The second limitation is that Future provides no composition operators. If you have Future<A> and want to transform the result into Future<B>, or combine two futures into one, or chain a second async operation after the first completes, there is no standard way to express this without blocking. You must block in get(), compute the transformation synchronously, and optionally submit a new task to get another Future. This makes it impossible to build non-blocking asynchronous pipelines with Future alone. The third limitation is exception handling. Future wraps all task exceptions in ExecutionException, but provides no operator to transform or recover from exceptions without blocking. There is no equivalent of catch() or exceptionally() on a Future. Error handling is an afterthought — you must always call get() to discover whether an exception occurred, which requires blocking. Future remains appropriate in specific scenarios where its limitations do not apply. When you need to submit a task and block for its result in a simple, linear fashion — particularly in tests, in batch jobs where the calling thread has nothing else to do while waiting, or in cases where there is exactly one asynchronous operation and no composition is needed — Future is the simplest and most explicit tool. It is also appropriate when invokeAll() or ExecutorCompletionService handle the complexity of multiple futures. For any scenario involving callbacks, non-blocking chaining, transformation, composition, or non-blocking exception handling, CompletableFuture is the right tool.
Java
// ── 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();

Related Topics in Multithreading

Process vs Thread
A process is an independent program in execution with its own isolated memory space, file handles, and system resources, managed by the operating system and separated from all other processes by strict boundaries. A thread is a unit of execution that lives inside a process, sharing that process's memory, heap, and resources with every other thread in the same process. Java programs run inside a JVM process; the JVM itself creates and manages threads, and every Java application starts with at least one thread — the main thread — with additional threads created by the JVM for garbage collection, JIT compilation, signal handling, and other runtime tasks. Understanding the distinction between processes and threads is the foundation for all concurrent programming in Java: it determines what is shared and what is isolated, what is fast and what is expensive, what fails independently and what fails together. This entry covers the OS-level and JVM-level model of processes and threads, the memory model that follows from the shared-versus-isolated distinction, the cost model for creation and context switching, failure isolation and its consequences, inter-process and inter-thread communication mechanisms, and the practical decision of when to use multiple processes versus multiple threads.
Thread Basics
A Java thread is an instance of java.lang.Thread that represents an independent path of execution within the JVM process. Every thread has a lifecycle — from creation through runnable, running, blocked, waiting, timed-waiting, and terminated states — and a set of properties including its name, priority, daemon status, thread group, and uncaught exception handler. The Java memory model specifies what visibility guarantees exist between threads and when writes by one thread are guaranteed to be visible to another. Thread scheduling is controlled by the OS scheduler subject to hints from the JVM via thread priority; the JVM does not provide real-time scheduling guarantees. This entry covers the complete thread lifecycle and its state machine, thread properties and how they affect scheduling and JVM shutdown, the happens-before relationship and why it matters for visibility, daemon threads and their relationship to JVM shutdown, thread interruption as a cooperative cancellation mechanism, and the methods on Thread that every Java developer must understand.
Creating Threads
Java provides three primary abstractions for defining the work a thread will execute: the Thread class itself (subclassed to override run()), the Runnable interface (a task with no return value and no checked exception), and the Callable interface (a task with a return value and a declared checked exception). Each represents a different contract between the task and the infrastructure that runs it. Thread subclassing couples the task definition to the execution mechanism and is the oldest and least flexible approach. Runnable decouples the task from the thread, allowing the same Runnable to be submitted to thread pools, scheduled executors, or wrapped in Thread objects. Callable extends that decoupling to include a return value and exception propagation, returning a Future that allows the caller to retrieve the result or handle exceptions asynchronously. Understanding all three — their contracts, their limitations, and when to use each — is the foundation of concurrent programming in Java before reaching for higher-level constructs.
Thread Lifecycle
The Java thread lifecycle is the complete sequence of states a thread passes through from the moment a Thread object is constructed to the moment its execution ends. Java defines six states in the Thread.State enum — NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, and TERMINATED — and the JVM transitions threads between these states in response to specific method calls, lock acquisitions, monitor notifications, timeouts, and exceptions. Each state has a precise meaning, a defined set of entry conditions, and a defined set of exit conditions. Understanding the lifecycle in full is prerequisite knowledge for diagnosing deadlocks, thread leaks, performance bottlenecks in thread dumps, and incorrect synchronization — all of which manifest as threads stuck in specific states. This entry covers every state in the lifecycle with its entry and exit conditions, all legal and illegal state transitions, how thread dumps represent each state, the interaction between lifecycle states and interruption, the effect of uncaught exceptions on lifecycle, and how to observe lifecycle transitions programmatically.