Fundamentals 23 min read

Master Java Concurrency: Callable, Future & FutureTask Explained

This article explores Java's thread creation methods, contrasts Runnable and Callable, delves into the ExecutorService API, and provides an in‑depth analysis of Future, FutureTask, and their internal state management, complete with practical code examples and performance‑optimizing techniques for concurrent programming.

Programmer DD
Programmer DD
Programmer DD
Master Java Concurrency: Callable, Future & FutureTask Explained

Introduction

Creating threads can be done by extending Thread or implementing Runnable, but both approaches lack parameters, return values, and exception propagation, making them "three‑no" solutions.

Extending Thread Implementing

Runnable

Callable

Introduced in Java 1.5 by Doug Lea, Callable is a generic functional interface with a single call() method that can return a value and throw checked exceptions.

public interface Callable<V> {
    V call() throws Exception;
}

Example usage:

Callable<String> callable = () -> {
    Thread.sleep(2000);
    return "Return some result";
};

Runnable vs Callable

Both are functional interfaces, but Runnable has no return value or checked‑exception support, while Callable does.

Execution Mechanism

Runnable

can be used with Thread or ExecutorService. Callable can only be used with ExecutorService because the Thread class does not have a call() method.

Exception Handling

Runnable.run()

cannot declare throws, so checked exceptions cannot be propagated. Callable.call() declares throws Exception, allowing exception handling.

ExecutorService

Key methods:

void execute(Runnable command);
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
execute()

returns no result, while all submit() overloads return a Future.

Future

Future

is an interface with five methods for task control and result retrieval.

// Cancel task
boolean cancel(boolean mayInterruptIfRunning);
// Get result (blocking)
V get() throws InterruptedException, ExecutionException;
// Get result with timeout
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
// Check cancellation
boolean isCancelled();
// Check completion
boolean isDone();

Example:

@Slf4j
public class FutureAndCallableExample {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        Callable<String> callable = () -> {
            log.info("enter Callable.call()");
            Thread.sleep(5000);
            return "Hello from Callable";
        };
        log.info("submit Callable");
        Future<String> future = executor.submit(callable);
        log.info("main thread continues");
        String result = future.get();
        log.info("main thread got result: {}", result);
        executor.shutdown();
    }
}

Using isDone() you can poll task status before blocking.

while (!future.isDone()) {
    System.out.println("Task is still not done...");
    Thread.sleep(1000);
}

When a task is cancelled, get() throws CancellationException.

FutureTask

FutureTask

implements RunnableFuture, which extends both Runnable and Future. It can wrap either a Callable or a Runnable (via Executors.callable).

public class FutureTask<V> implements RunnableFuture<V> {
    // core fields
    private Callable<V> callable;
    private volatile int state;
    private Object outcome; // result or exception
    ...
}

The run() method executes the underlying Callable, captures the result with set(result) or stores an exception with setException(ex), and then triggers finishCompletion() to wake waiting threads.

public void run() {
    if (state != NEW || !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread()))
        return;
    try {
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                result = c.call();
                ran = true;
            } catch (Throwable ex) {
                result = null;
                ran = false;
                setException(ex);
            }
            if (ran) set(result);
        }
    } finally {
        runner = null;
        int s = state;
        if (s >= INTERRUPTING) handlePossibleCancellationInterrupt(s);
    }
}

State transitions (NEW → COMPLETING → NORMAL/EXCEPTIONAL, or NEW → CANCELLED/INTERRUPTING → INTERRUPTED) are managed via CAS to ensure thread‑safety.

private static final int NEW = 0;
private static final int COMPLETING = 1;
private static final int NORMAL = 2;
private static final int EXCEPTIONAL = 3;
private static final int CANCELLED = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED = 6;

The waiting queue is a Treiber stack of WaitNode objects; awaitDone() parks the current thread until the task reaches a final state, then finishCompletion() unparks all waiting threads.

private void finishCompletion() {
    for (WaitNode q; (q = waiters) != null;) {
        if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
            for (;;) {
                Thread t = q.thread;
                if (t != null) {
                    q.thread = null;
                    LockSupport.unpark(t);
                }
                WaitNode next = q.next;
                if (next == null) break;
                q.next = null;
                q = next;
            }
            break;
        }
    }
    done();
    callable = null;
}

Task termination can be triggered by set(), setException(), or cancel(). The cancel() implementation uses CAS to move from NEW to either INTERRUPTING or CANCELLED, optionally interrupting the running thread.

public boolean cancel(boolean mayInterruptIfRunning) {
    if (!(state == NEW && UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
            mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
        return false;
    try {
        if (mayInterruptIfRunning) {
            try {
                Thread t = runner;
                if (t != null) t.interrupt();
            } finally {
                UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
            }
        }
    } finally {
        finishCompletion();
    }
    return true;
}

Practical Example – Making Tea with FutureTask

The following program demonstrates how two parallel tasks (boiling water and preparing tea accessories) can be coordinated using FutureTask and an ExecutorService to reduce total execution time from 20 minutes to 16 minutes.

@Slf4j
public class MakeTeaExample {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executor = Executors.newFixedThreadPool(2);
        FutureTask<String> ft1 = new FutureTask<>(new T1Task());
        FutureTask<String> ft2 = new FutureTask<>(new T2Task());
        executor.submit(ft1);
        executor.submit(ft2);
        log.info(ft1.get() + ft2.get());
        log.info("Start brewing tea");
        executor.shutdown();
    }
    static class T1Task implements Callable<String> {
        @Override public String call() throws Exception {
            log.info("T1: wash kettle");
            TimeUnit.SECONDS.sleep(1);
            log.info("T1: boil water");
            TimeUnit.SECONDS.sleep(15);
            return "T1: water ready";
        }
    }
    static class T2Task implements Callable<String> {
        @Override public String call() throws Exception {
            log.info("T2: wash teapot");
            TimeUnit.SECONDS.sleep(1);
            log.info("T2: wash cup");
            TimeUnit.SECONDS.sleep(2);
            log.info("T2: fetch tea leaves");
            TimeUnit.SECONDS.sleep(1);
            return "T2: tea leaves ready";
        }
    }
}

By letting the longer‑running water‑boiling task wait for the tea‑preparation task’s result via ft2.get(), the program achieves optimal parallelism.

Summary

Java’s evolution introduced Callable to fill the gap left by Runnable, enabling return values and checked‑exception handling. Future and its primary implementation FutureTask provide a robust mechanism for asynchronous result retrieval, cancellation, and state inspection. Understanding their internal state machine and waiting‑queue logic empowers developers to write efficient, non‑blocking concurrent code, and paves the way for higher‑level abstractions such as CompletableFuture introduced in Java 8.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaconcurrencyThreadPoolCallableFuture
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.