data-driven-docs

Living documentation for evolving technologies

View on GitHub

Callable and Future

Advanced Java 8+ Est. Time: 12min


Table of Contents


Overview

Callable and Future work together to enable asynchronous computation with return values.

Key Concepts:

This pattern was the foundation for asynchronous programming in Java before CompletableFuture arrived in Java 8.

Back to top


Callable vs Runnable

Runnable (Java 1.0)

public interface Runnable {
    void run(); // No return value, cannot throw checked exceptions
}

Limitations:

Callable (Java 5+)

public interface Callable<V> {
    V call() throws Exception; // Returns value, can throw exceptions
}

Benefits:

Comparison Example

With Runnable:

// Need shared state to capture result
final List<Integer> result = new ArrayList<>();
final List<Exception> errors = new ArrayList<>();

Runnable task = () -> {
    try {
        int value = performCalculation();
        result.add(value);
    } catch (Exception e) {
        errors.add(e);
    }
};

With Callable:

// Clean return value and exception propagation
Callable<Integer> task = () -> {
    return performCalculation(); // Exception automatically propagated
};

Back to top


Future Interface

Future<V> represents the result of an asynchronous computation.

Core Methods:

public interface Future<V> {
    // Check status
    boolean isDone();
    boolean isCancelled();

    // Get result (blocks until available)
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;

    // Cancel execution
    boolean cancel(boolean mayInterruptIfRunning);
}

Method Descriptions:

Back to top


Working with Callable and Future

Basic Example

import java.util.concurrent.*;

public class CallableExample {
    public static void main(String[] args) throws Exception {
        ExecutorService executor = Executors.newSingleThreadExecutor();

        // Define a Callable task
        Callable<Integer> task = () -> {
            System.out.println("Task started on: " +
                             Thread.currentThread().getName());
            Thread.sleep(2000); // Simulate long computation
            return 42;
        };

        // Submit and get Future
        Future<Integer> future = executor.submit(task);

        System.out.println("Task submitted, doing other work...");

        // Do other work while task executes
        doOtherWork();

        // Get result (blocks if not ready)
        Integer result = future.get();
        System.out.println("Result: " + result);

        executor.shutdown();
    }

    static void doOtherWork() {
        System.out.println("Doing other work on main thread");
    }
}

Output:

Task submitted, doing other work...
Doing other work on main thread
Task started on: pool-1-thread-1
Result: 42

Multiple Callable Tasks

ExecutorService executor = Executors.newFixedThreadPool(3);
List<Callable<String>> tasks = Arrays.asList(
    () -> { Thread.sleep(1000); return "Task 1"; },
    () -> { Thread.sleep(2000); return "Task 2"; },
    () -> { Thread.sleep(1500); return "Task 3"; }
);

// Submit all tasks
List<Future<String>> futures = new ArrayList<>();
for (Callable<String> task : tasks) {
    futures.add(executor.submit(task));
}

// Collect results
for (Future<String> future : futures) {
    System.out.println(future.get()); // Blocks for each
}

executor.shutdown();

Using invokeAll (Bulk Submission)

ExecutorService executor = Executors.newFixedThreadPool(3);

List<Callable<String>> tasks = Arrays.asList(
    () -> { Thread.sleep(1000); return "Task 1"; },
    () -> { Thread.sleep(2000); return "Task 2"; },
    () -> { Thread.sleep(1500); return "Task 3"; }
);

// Submit all tasks and wait for all to complete
List<Future<String>> futures = executor.invokeAll(tasks);

// All tasks are done at this point
for (Future<String> future : futures) {
    System.out.println(future.get()); // Non-blocking, already complete
}

executor.shutdown();

Using invokeAny (First to Complete)

ExecutorService executor = Executors.newFixedThreadPool(3);

List<Callable<String>> tasks = Arrays.asList(
    () -> { Thread.sleep(3000); return "Slow task"; },
    () -> { Thread.sleep(1000); return "Fast task"; },
    () -> { Thread.sleep(2000); return "Medium task"; }
);

// Returns result of first task to complete successfully
String result = executor.invokeAny(tasks);
System.out.println(result); // "Fast task"

executor.shutdown();

Back to top


Getting Results

Blocking with get()

Future<String> future = executor.submit(() -> {
    Thread.sleep(5000);
    return "Result";
});

// Blocks for up to 5 seconds
String result = future.get();

get() with Timeout

Future<String> future = executor.submit(() -> {
    Thread.sleep(5000);
    return "Result";
});

try {
    // Wait max 2 seconds
    String result = future.get(2, TimeUnit.SECONDS);
    System.out.println(result);
} catch (TimeoutException e) {
    System.out.println("Task timed out!");
    future.cancel(true); // Cancel the slow task
}

Polling with isDone()

Future<Integer> future = executor.submit(() -> {
    Thread.sleep(2000);
    return 100;
});

// Non-blocking poll
while (!future.isDone()) {
    System.out.println("Task still running...");
    Thread.sleep(500);
}

Integer result = future.get(); // Won't block, already done
System.out.println("Result: " + result);

Back to top


Cancellation and Interruption

Canceling a Task

Future<String> future = executor.submit(() -> {
    for (int i = 0; i < 10; i++) {
        if (Thread.currentThread().isInterrupted()) {
            System.out.println("Task was interrupted");
            return "Cancelled";
        }
        Thread.sleep(1000);
        System.out.println("Working... " + i);
    }
    return "Completed";
});

// Cancel after 3 seconds
Thread.sleep(3000);
boolean cancelled = future.cancel(true); // true = interrupt if running
System.out.println("Cancelled: " + cancelled);

try {
    future.get(); // Throws CancellationException
} catch (CancellationException e) {
    System.out.println("Task was cancelled");
}

cancel() Parameters:

Handling Interruption in Task

Callable<String> task = () -> {
    try {
        for (int i = 0; i < 100; i++) {
            // Check for interruption
            if (Thread.currentThread().isInterrupted()) {
                throw new InterruptedException("Task interrupted");
            }
            // Do work
            performWork(i);
        }
        return "Success";
    } catch (InterruptedException e) {
        System.out.println("Cleaning up after interruption");
        return "Interrupted";
    }
};

Back to top


Limitations

Despite being useful, Callable and Future have significant limitations that led to CompletableFuture:

1. Blocking API

// get() blocks the calling thread
String result = future.get(); // Must wait!

No way to attach a callback or continue with a non-blocking operation.

2. No Chaining

// Cannot chain operations
Future<Integer> future1 = executor.submit(() -> 10);
// How to transform result to String? Need manual code:
Integer result = future1.get();
Future<String> future2 = executor.submit(() -> String.valueOf(result));

3. No Combining

// Cannot easily combine results from multiple futures
Future<Integer> f1 = executor.submit(() -> 10);
Future<Integer> f2 = executor.submit(() -> 20);

// Manual combination needed
Integer sum = f1.get() + f2.get(); // Blocks twice

4. Limited Exception Handling

try {
    String result = future.get();
} catch (ExecutionException e) {
    // Wrapped exception, needs unwrapping
    Throwable cause = e.getCause();
}

5. No Completion Notification

No built-in way to be notified when computation completes without polling or blocking.

Solution: Use CompletableFuture for advanced asynchronous patterns (Java 8+).

Back to top


Future Execution Flow

Client: return Future note over Client: Can do other work

Executor -> WorkerThread: assign task WorkerThread -> WorkerThread: execute callable.call()

alt Task completes successfully WorkerThread -> Future: set result(value) Client -> Future: get() Future –> Client: return value else Task throws exception WorkerThread -> Future: set exception(e) Client -> Future: get() Future –> Client: throw ExecutionException else Task is cancelled Client -> Future: cancel(true) Future -> WorkerThread: interrupt() WorkerThread -> Future: set cancelled Client -> Future: get() Future –> Client: throw CancellationException end @enduml –>

Future Execution Flow

Back to top


Ref.

Official Documentation:

Guides:

Next Steps:


Get Started | Java Concurrency | Java 8