PrepZone Logo
PrepZone

Callable, Future and Fork/Join

Returning values from tasks, where Future falls short, and how fork/join splits big work.

Read these first

Why this matters

  • Most real tasks produce a result or fail, and Runnable can express neither — it cannot return a value and cannot throw a checked exception.
  • A Future that silently holds an exception nobody ever calls get() on is a genuinely common way to lose an error.
  • Understanding why Future is limited is what makes CompletableFuture feel like a solution rather than extra API.
supplyAsync — fetch user
supplyAsync — fetch orders
Build the responseRuns when both finish
Fallback valueFailure handled in the chain
Each stage declares what happens next instead of blocking for a result. Independent calls run in parallel and only join where you actually combine them.

Callable

Java
// Runnable: no result, no checked exception
Runnable task = () -> System.out.println("done");

// Callable: returns a value and may throw
Callable<Integer> job = () -> {
    Thread.sleep(100);              // a checked exception, allowed here
    return 42;
};
AspectRunnableCallable
Methodvoid run()V call() throws Exception
Returns a valueNoYes
Checked exceptionsCannot throw anyMay throw anything
Added inJava 1.0Java 5
Submit withexecute() or submit()submit()
  • Method

    Runnablevoid run()
    CallableV call() throws Exception
  • Returns a value

    RunnableNo
    CallableYes
  • Checked exceptions

    RunnableCannot throw any
    CallableMay throw anything
  • Added in

    RunnableJava 1.0
    CallableJava 5
  • Submit with

    Runnableexecute() or submit()
    Callablesubmit()

Callable is the one to reach for whenever a task produces a result or can fail meaningfully.

Future

Submitting a Callable returns a handle to a result that does not exist yet.

Java
try (ExecutorService pool = Executors.newFixedThreadPool(4)) {
    Future<Integer> future = pool.submit(() -> expensiveComputation());

    doSomethingElse();                        // runs in parallel

    Integer result = future.get();            // blocks until the result is ready
    Integer bounded = future.get(2, TimeUnit.SECONDS);   // or gives up
}

The five methods

  • get() — blocks until the task completes, then returns the value.
  • get(timeout, unit) — the same, but throws TimeoutException on expiry. Prefer this form.
  • isDone() — true once finished, whether by success, exception or cancellation.
  • cancel(mayInterruptIfRunning) — prevents a queued task from starting; interrupts a running one if the flag is true and the task cooperates.
  • isCancelled() — true if cancellation succeeded.

Exceptions arrive wrapped

Java
Future<Integer> future = pool.submit(() -> { throw new IllegalStateException("failed"); });

try {
    future.get();
} catch (ExecutionException e) {
    Throwable cause = e.getCause();          // the IllegalStateException
    log.error("Task failed", cause);
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

Anything the task throws is wrapped in ExecutionException. Always unwrap with getCause() before logging, or the message says nothing useful.

Why Future is limited

Java
// This is the fundamental problem: you must block to find out anything
Future<User> user = pool.submit(() -> fetchUser(id));
Future<Orders> orders = pool.submit(() -> fetchOrders(id));

User u = user.get();                 // the thread sits idle here
Orders o = orders.get();             // and again here

// There is no way to say "when both are ready, combine them, without blocking"

What it cannot do

  • No completion callback. You cannot register "run this when the result arrives"; you can only poll or block.
  • No chaining. Feeding one result into the next task means blocking in between.
  • No combining. Waiting for two futures means two sequential blocking calls.
  • No manual completion. You cannot complete a Future from outside the task.
  • Clumsy error handling. Recovery means catching ExecutionException and unwrapping it.

CompletableFuture, covered in the Java 8 module, addresses every one of these. It is worth knowing this list because it is exactly the motivation for that API.

invokeAll and invokeAny

Submitting a batch is cleaner than a loop of submit calls.

Java
List<Callable<Integer>> jobs = List.of(
        () -> fetchFromCache(),
        () -> fetchFromDatabase(),
        () -> fetchFromApi());

// Waits for every task; returns futures that are all already done
List<Future<Integer>> all = pool.invokeAll(jobs);
for (Future<Integer> future : all) {
    System.out.println(future.get());          // no longer blocks
}

// Returns the first successful result and cancels the rest
Integer fastest = pool.invokeAny(jobs);

invokeAny is useful when several sources can answer the same question and you want whichever responds first. invokeAll accepts a timeout, after which unfinished tasks are cancelled.

ScheduledExecutorService

Java
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);

scheduler.schedule(() -> sendReminder(), 5, TimeUnit.MINUTES);           // once

scheduler.scheduleAtFixedRate(this::collectMetrics, 0, 1, TimeUnit.MINUTES);
// Starts every minute regardless of how long a run takes

scheduler.scheduleWithFixedDelay(this::pollQueue, 0, 1, TimeUnit.SECONDS);
// Waits one second AFTER each run finishes

Fork/Join

For a problem that splits recursively into independent subproblems, ForkJoinPool adds work stealing: an idle thread takes queued work from a busy thread's deque.

Java
public class SumTask extends RecursiveTask<Long> {
    private static final int THRESHOLD = 10_000;
    private final long[] values;
    private final int from, to;

    SumTask(long[] values, int from, int to) {
        this.values = values; this.from = from; this.to = to;
    }

    @Override
    protected Long compute() {
        if (to - from <= THRESHOLD) {
            long sum = 0;
            for (int i = from; i < to; i++) sum += values[i];
            return sum;                              // small enough: just do it
        }

        int mid = (from + to) / 2;
        SumTask left = new SumTask(values, from, mid);
        SumTask right = new SumTask(values, mid, to);

        left.fork();                                  // queue the left half
        long rightResult = right.compute();           // compute the right half here
        return rightResult + left.join();             // then collect the left
    }
}
Java
long total = ForkJoinPool.commonPool().invoke(new SumTask(values, 0, values.length));

The details that matter

  • RecursiveTask<V> returns a value; RecursiveAction does not.
  • Fork one half, compute the other on the current thread. Forking both wastes a thread doing nothing but waiting.
  • The threshold matters. Too small and coordination overhead dominates; too large and parallelism is lost. Benchmark it.
  • The common pool is shared across the JVM and sized to cores minus one. Parallel streams use it too.
  • Never block inside a fork/join task. A blocking call occupies a pool thread that cannot be stolen from, and with the common pool you are stalling parallel streams elsewhere in the application.

Common misreadings

  • "Runnable can return a value through a field." It can, but then you have to coordinate reading it safely. Callable exists for exactly this.
  • "future.get() runs the task." The executor already started it. get() only waits.
  • "cancel(true) kills the task." It interrupts the thread. A task that ignores interruption keeps running.
  • "isDone() means success." It is also true after an exception or a cancellation.
  • "scheduleAtFixedRate and scheduleWithFixedDelay are equivalent." They diverge sharply when a run overruns its period.
  • "A scheduled task recovers from an exception." It does not — all future executions are cancelled silently.
  • "Fork/join is faster for any parallel work." It is for recursive CPU-bound division. Blocking inside it is actively harmful.

Quick recall

Everything you need if you only revisit this box.

  • Callable<V> returns a value and may throw checked exceptions; Runnable does neither.
  • submit returns a Future. Prefer get(timeout, unit) over the unbounded get().
  • Task exceptions are wrapped in ExecutionException — unwrap with getCause(), and remember that a Future nobody reads hides the failure entirely.
  • Future cannot notify, chain, combine or be completed externally. That list is the motivation for CompletableFuture.
  • invokeAll waits for all; invokeAny returns the first success and cancels the rest.
  • For polling use scheduleWithFixedDelay, and always catch exceptions inside a scheduled task or it stops permanently.
  • Fork/join uses work stealing: fork one half, compute the other, tune the threshold, and never block inside a task.

Test yourself

Answer these before moving on — recall is what makes it stick.