Concurrent Utilities

Futures & Results

15 min Lesson 3 of 13

Futures & Results

When you submit a task to an ExecutorService, the call returns immediately — but the task keeps running in the background. The bridge between that background work and your code is a Future<V>: a handle on a computation that has not finished yet. Understanding how to retrieve, time-limit, and cancel those computations is essential for writing correct concurrent programs.

What is a Future?

java.util.concurrent.Future<V> is a generic interface with five methods. The type parameter V is the type of the result when the computation completes.

import java.util.concurrent.*; ExecutorService pool = Executors.newCachedThreadPool(); Future<Integer> future = pool.submit(() -> { // simulates work taking time Thread.sleep(500); return 42; }); // --- your thread continues here while the pool thread works --- Integer result = future.get(); // blocks until done System.out.println("Result: " + result); // Result: 42 pool.shutdown();

submit(Callable<V>) returns a Future<V>. The submitted Callable runs on a pool thread; future.get() blocks the calling thread until the result is ready.

The Five Methods of Future

  • get() — blocks indefinitely until done, then returns the result.
  • get(long timeout, TimeUnit unit) — blocks up to the given duration; throws TimeoutException if it expires.
  • cancel(boolean mayInterruptIfRunning) — attempts to cancel; returns false if already done or un-cancellable.
  • isCancelled() — returns true if cancelled before it completed.
  • isDone() — returns true if finished (normally, via cancellation, or by exception).

Blocking with a Timeout

Blocking forever with get() is almost always wrong in real applications — a hung task would stall your thread indefinitely. Prefer the timeout overload:

Future<String> future = pool.submit(() -> fetchFromRemoteService()); try { String value = future.get(2, TimeUnit.SECONDS); System.out.println("Got: " + value); } catch (TimeoutException e) { System.err.println("Service too slow — aborting"); future.cancel(true); // interrupt the running thread } catch (InterruptedException e) { Thread.currentThread().interrupt(); // restore interrupt flag } catch (ExecutionException e) { Throwable cause = e.getCause(); // the exception thrown inside the task cause.printStackTrace(); }
Always handle ExecutionException. If the Callable throws any checked or unchecked exception, get() wraps it in an ExecutionException. Ignoring this means silently swallowing errors. Always call e.getCause() to get the real problem.

Cancellation in Depth

The boolean passed to cancel() matters:

  • cancel(false) — if the task has not started yet, prevent it from ever starting; if it is already running, let it finish.
  • cancel(true) — additionally send an interrupt signal to the thread currently running the task.

Interruption only works if the running code cooperates — it must either call a blocking method that throws InterruptedException, or periodically check Thread.currentThread().isInterrupted().

Future<Void> longTask = pool.submit(() -> { for (int i = 0; i < 1_000_000; i++) { if (Thread.currentThread().isInterrupted()) { System.out.println("Interrupted at step " + i); return null; } doWork(i); } return null; }); Thread.sleep(100); longTask.cancel(true); // signals the thread
Write interruptible tasks. Always check the interrupt flag inside long loops, and propagate or restore it on InterruptedException. Tasks that ignore interrupts cannot be cancelled cleanly.

Checked Exceptions from get()

get() declares three checked exceptions you must handle:

  • InterruptedException — the waiting thread itself was interrupted. Always restore the flag: Thread.currentThread().interrupt().
  • ExecutionException — the task threw an exception. Unwrap with getCause().
  • TimeoutException (timeout overload only) — the deadline passed before the result arrived.

FutureTask: Combining Runnable and Future

FutureTask<V> implements both Runnable and Future<V>. You can wrap a Callable in it, submit it to an executor, and still hold a typed Future reference — which is useful when you need to pass the task object around before submitting it:

FutureTask<Long> task = new FutureTask<>(() -> computeExpensiveValue()); new Thread(task).start(); // or pool.execute(task) long result = task.get(); // blocks until done

Polling vs Blocking

You can avoid blocking entirely if you have other work to do while waiting:

Future<Data> f1 = pool.submit(() -> loadFromDatabase()); Future<Data> f2 = pool.submit(() -> loadFromNetwork()); // do other local work here ... Data db = f1.get(); // block only when you actually need the result Data net = f2.get();

Both tasks run in parallel. You only block when you genuinely need the results, not at submission time.

Future is deliberately simple — and limited. You cannot attach a callback, chain transformations, or combine multiple futures without blocking threads. That is exactly why CompletableFuture was introduced in Java 8. The next two lessons cover it in depth.

Summary

Future<V> gives you a handle on an asynchronous computation. Use the timeout overload of get() in production code, always handle ExecutionException by inspecting its cause, and call cancel(true) to abort runaway tasks — but only if your task cooperates with interruption. For richer composition, CompletableFuture is the next step.