Waiting for Results

Priya’s thread pool handles many orders at once. Good. But some steps need an answer back. Before confirming a sale, QuickCart must check the supplier’s price. That call takes a second. Priya submits the check to a worker — then she needs the result, not just “task started.”

Runnable vs Callable

Runnable runs code but returns nothing:

Runnable task = new Runnable() {
    @Override
    public void run() {
        System.out.println("Task executing");
        // No return value
    }
};

ExecutorService executor = Executors.newFixedThreadPool(5);
Future<?> future = executor.submit(task);
Object result = future.get(); // Always null

Callable runs code and returns a value. It can also throw checked exceptions:

Callable<String> task = new Callable<String>() {
    @Override
    public String call() throws Exception {
        Thread.sleep(1000);
        return "Result";
    }
};

Future<String> future = executor.submit(task);
String result = future.get(); // "Result"

Lambda style:

Future<String> future = executor.submit(() -> {
    Thread.sleep(1000);
    return "Task completed";
});
Runnable Callable
Return value No Yes
Checked exceptions Cannot throw Can throw
Method name run() call()
Future type Future<?> Future<T>

When you need data back, use Callable.

Future interface

When you submit a Callable, you get a Future — a handle to a result that is not ready yet.

public interface Future<V> {
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
    boolean cancel(boolean mayInterruptIfRunning);
    boolean isCancelled();
    boolean isDone();
}

Getting the result

Method 1: Blocking get

Waits until the worker finishes:

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

String result = future.get();
System.out.println(result); // "Result"

Method 2: Get with timeout

Do not wait forever. Always prefer a timeout in production code:

try {
    String result = future.get(5, TimeUnit.SECONDS);
    System.out.println(result);
} catch (TimeoutException e) {
    System.out.println("Task timed out");
    future.cancel(true); // Cancel if still running
}

Method 3: Non-blocking check

if (future.isDone()) {
    String result = future.get();
    System.out.println(result);
} else {
    System.out.println("Still running...");
}

Method 4: Polling

while (!future.isDone()) {
    System.out.println("Waiting for supplier price...");
    Thread.sleep(100);
}

String result = future.get();

Handling exceptions

Exceptions inside a Callable do not crash the caller immediately. They are wrapped in ExecutionException when you call get():

Callable<String> task = () -> {
    if (priceUnavailable) {
        throw new RuntimeException("Supplier API down");
    }
    return "Rs 499";
};

Future<String> future = executor.submit(task);

try {
    String price = future.get(5, TimeUnit.SECONDS);
} catch (ExecutionException e) {
    Throwable cause = e.getCause(); // The real exception
    System.out.println("Price check failed: " + cause.getMessage());
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
} catch (TimeoutException e) {
    future.cancel(true);
}

Checked exceptions

Callable can throw checked exceptions like IOException:

Callable<String> task = () -> {
    throw new IOException("Supplier file not found");
};

Future<String> future = executor.submit(task);

try {
    String result = future.get();
} catch (ExecutionException e) {
    if (e.getCause() instanceof IOException) {
        IOException ioException = (IOException) e.getCause();
        // Handle IOException
    }
}

Always unwrap ExecutionException with getCause() to see what actually went wrong.

Cancellation

Cancel a running task

Future<String> future = executor.submit(() -> {
    Thread.sleep(10000); // Long-running task
    return "Result";
});

boolean cancelled = future.cancel(true); // true = interrupt if running

if (cancelled) {
    System.out.println("Task cancelled");
}

Check cancellation status

if (future.isCancelled()) {
    System.out.println("Task was cancelled");
} else if (future.isDone()) {
    try {
        String result = future.get();
    } catch (ExecutionException e) {
        // Handle exception
    }
}

Calling get() on a cancelled Future throws CancellationException.

Handle cancellation inside the task

The worker must check the interrupt flag:

Callable<String> task = () -> {
    while (!Thread.currentThread().isInterrupted()) {
        // Keep trying supplier...
        Thread.sleep(100);
    }
    throw new InterruptedException("Task cancelled");
};

Future<String> future = executor.submit(task);

Thread.sleep(5000);
future.cancel(true); // Interrupts the worker thread

QuickCart: parallel price checks

A customer buys three items from different suppliers. Priya checks all prices at once:

public class PriceChecker {
    private final ExecutorService pool = Executors.newFixedThreadPool(5);

    public Map<String, Double> checkPrices(List<String> productIds) throws Exception {
        List<Callable<Double>> tasks = new ArrayList<>();

        for (String productId : productIds) {
            tasks.add(() -> fetchPriceFromSupplier(productId));
        }

        List<Future<Double>> futures = pool.invokeAll(tasks);

        Map<String, Double> prices = new HashMap<>();
        for (int i = 0; i < futures.size(); i++) {
            try {
                prices.put(productIds.get(i), futures.get(i).get());
            } catch (ExecutionException e) {
                System.err.println("Failed for " + productIds.get(i)
                    + ": " + e.getCause().getMessage());
            }
        }
        return prices;
    }

    private Double fetchPriceFromSupplier(String productId) throws InterruptedException {
        Thread.sleep(800); // Simulates an API call
        return 99.0 + productId.hashCode() % 50;
    }
}

All three supplier calls run in parallel. Total wait time is roughly one call, not three.

QuickCart: parallel order validation

Priya validates several cart rules at once, each returning a decision:

public class OrderValidator {
    private final ExecutorService pool = Executors.newFixedThreadPool(10);

    public List<String> validateOrder(String orderId, List<String> checks) {
        List<Future<String>> futures = new ArrayList<>();

        for (String check : checks) {
            Callable<String> task = () -> runCheck(orderId, check);
            futures.add(pool.submit(task));
        }

        List<String> results = new ArrayList<>();
        for (Future<String> future : futures) {
            try {
                results.add(future.get(5, TimeUnit.SECONDS));
            } catch (Exception ex) {
                System.err.println("Validation failed: " + ex.getMessage());
                results.add("FAILED");
            }
        }
        return results;
    }

    private String runCheck(String orderId, String check) {
        // Run one validation rule, return pass/fail message
        return check + ": OK for " + orderId;
    }
}

invokeAll — fan-out, then collect

invokeAll submits every task and waits until all finish:

List<Callable<String>> tasks = Arrays.asList(
    () -> "Task 1",
    () -> "Task 2",
    () -> "Task 3"
);

List<Future<String>> futures = executor.invokeAll(tasks);

// All tasks are done
for (Future<String> future : futures) {
    String result = future.get();
    System.out.println(result);
}

With a timeout:

try {
    List<Future<String>> futures = executor.invokeAll(tasks, 5, TimeUnit.SECONDS);
    // All completed within 5 seconds
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

This is the classic fan-out pattern: send work to many workers, gather all results.

invokeAny — first result wins

When you only need the fastest answer — like checking two payment gateways:

List<Callable<String>> tasks = Arrays.asList(
    () -> {
        Thread.sleep(3000);
        return "Gateway A: approved";
    },
    () -> {
        Thread.sleep(1000);
        return "Gateway B: approved"; // Fastest
    },
    () -> {
        Thread.sleep(2000);
        return "Gateway C: approved";
    }
);

String result = executor.invokeAny(tasks);
System.out.println(result); // "Gateway B: approved"

invokeAny returns the result of the first completed task. The others may still run unless you cancel them.

What to remember

  • Use Callable when you need a return value.
  • Future.get() blocks — always use a timeout in real code.
  • Catch ExecutionException and read getCause() for the real error.
  • invokeAll is perfect for “run these in parallel, then collect everything.”
  • invokeAny is for “give me the first answer that comes back.”
  • Cancel tasks that take too long with future.cancel(true).
  • Inside long-running tasks, check Thread.currentThread().isInterrupted().

What Priya does next: price checks feed into more steps — validate, charge, notify — and she wants to chain those steps without blocking the main thread on every get().