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().