Java Callable and Future
Table of Contents + β
In the last lesson you learned about Java ExecutorService and thread pool types. Those tasks were Runnable, which returns nothing. But often you want a background task to give back a result, and for that Java has Callable and Future.
π€ The limitation of Runnable
Picture this. You ask a helper thread to add up a long list of numbers while your main thread does other things. The helper does the math. Then you reach for the answer and there is no door to hand it through. That is the wall you hit with Runnable.
A Runnable does work but cannot return anything. Its run method returns void, meaning βno value at all.β So if a background task computes a number, there is no clean way to send it back.
This Runnable computes a value but cannot pass it back out.
Runnable task = () -> { int result = 2 + 2; // β computed, but how do we return it? // run() returns void, so the result is stuck inside the task};Letβs name what is going wrong here.
- The value lives inside the lambda, on the helper thread.
runreturnsvoid, so there is no return slot to put it in.- The main thread has no handle to reach in and grab it.
People sometimes work around this with a shared variable, but that path is full of timing and lock bugs just to read one number. We need a task that returns a value and a way to receive it later. That is what Callable and Future provide.
Runnable cannot do one more thing. Its run method cannot throw a checked exception, an error type Java forces you to declare or catch. So it cannot pass a failure back either. Callable solves both problems at once.
π§© Callable: a task that returns a value
A Callable is like a Runnable, but its method is named call, and call returns a result. It is also a functional interface, so you can write it as a lambda. The difference: call gives back a value and can throw a checked exception.
This Callable adds the numbers 1 through 100 and returns the total as an Integer.
import java.util.concurrent.Callable;
// β
a Callable that returns an IntegerCallable<Integer> task = () -> { int sum = 0; for (int i = 1; i <= 100; i++) sum += i; return sum; // β
Callable CAN return a value};Letβs read this line by line.
Callable<Integer>means βa task that produces an Integer when it runsβ.- The lambda body loops and builds up a sum.
- The
return sum;hands the answer back, which Runnable could never do.
The type inside the angle brackets is whatever the task produces. Text means Callable<String>. A list of orders means Callable<List<Order>>. The shape flexes to fit your work.
But the task runs on another thread, possibly long after you started it. So when call returns its value, where does it go? You need somewhere to hold it until you read it. That holding place is a Future.
π§© Future: a promise of a result
A Future represents a result that is not ready yet but will be. Submitting a Callable gives you a Future immediately. The task runs in the background, and later you ask the Future for the result.
Think of ordering food at a counter. You get a token. The food is not ready, but the token stands in for your order. When it is done, you use the token to collect it. A Future is that token. What it gives you:
- It is handed back the instant you submit, so your thread is never blocked at submit time.
- It holds the result the moment the background task finishes.
- It lets you decide when to collect that result.
So Callable is the worker that produces a value, and Future is the claim ticket. Now letβs put them together.
π οΈ Putting Callable and Future together
The flow is always the same. You submit a Callable to an ExecutorService, get a Future back, and call get() to fetch the result when you need it.
This program submits one task, prints a message while it runs, then collects the result.
import java.util.concurrent.Callable;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Future;
public class Main { public static void main(String[] args) throws Exception { ExecutorService pool = Executors.newFixedThreadPool(2);
Callable<Integer> task = () -> { int sum = 0; for (int i = 1; i <= 100; i++) sum += i; return sum; };
Future<Integer> future = pool.submit(task); // β
returns immediately System.out.println("Task submitted, doing other work...");
Integer result = future.get(); // waits here until the result is ready System.out.println("Result: " + result);
pool.shutdown(); }}Letβs walk through what each step is doing.
pool.submit(task)hands the task to the pool and instantly returns aFuture, without waiting for the math to finish.- The next line runs straight away, so the main thread is free to do real work in the meantime.
future.get()is the moment of truth. It waits until the result is ready, then returns it.- The sum of 1 to 100 is 5050, and that value comes back through the Future.
pool.shutdown()tells the pool no more tasks are coming, so the program can end cleanly.
Notice main declares throws Exception, because get() can throw. We cover that soon. For now, just know you must deal with it.
Output
Task submitted, doing other work...Result: 5050The order tells the whole story. The βdoing other workβ message prints before the result, because submit did not block. That gap is where your parallelism lives.
β³ How get() behaves
The key thing about get() is one word: it blocks. The calling thread stops and waits, doing nothing else, until the result is ready.
This snippet shows the pause point clearly.
Future<Integer> future = pool.submit(task);// ... main thread is free to do other things right here ...Integer result = future.get(); // β
blocks until the result is availableHere is how get() decides what to do.
- If the task is already finished,
get()returns the result instantly, with no wait. - If the task is still running, the calling thread pauses until the task finishes.
- Once the result arrives,
get()hands it back and your code carries on.
So the timing of your get() call matters. Do your other work first, then call get(), so the background task and your main thread overlap. That overlap is the whole point.
If you do not want to wait at all, the Future has other tools:
future.isDone()returns true or false right away, telling you whether the result is ready, without blocking.future.get(timeout, unit)waits, but only up to a limit. If the result is not ready in time it throws aTimeoutException, so a slow task cannot freeze you forever.future.cancel(true)asks the executor to stop the task if it has not finished. It returns true if the cancel request succeeded.future.isCancelled()tells you whether the task was cancelled before it completed.
This loop keeps the main thread busy and only collects the result once the task is done.
Future<Integer> future = pool.submit(task);
while (!future.isDone()) { System.out.println("Still working... doing other useful stuff"); // β
do real work here instead of just waiting}Integer result = future.get(); // result is ready, so this returns instantlySystem.out.println("Result: " + result);In real programs you would do meaningful work inside that loop. The point is that isDone() lets you check in without freezing.
get() waits, so call it at the right time
future.get() blocks the calling thread until the result is ready. If you call it right after submitting, you lose the parallelism, because you just sit and wait for the task to finish. Do your other work first, then call get() when you actually need the result.
π οΈ Submitting several tasks and collecting results
A thread pool shines when you run many tasks at once. Submit several Callables, keep all the Futures, then collect every result. The tasks run in parallel, so the total time is closer to the slowest single task than the sum of all of them.
This program submits three squaring tasks, then loops over the Futures to gather every answer.
import java.util.ArrayList;import java.util.List;import java.util.concurrent.Callable;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Future;
public class Main { public static void main(String[] args) throws Exception { ExecutorService pool = Executors.newFixedThreadPool(3); List<Future<Integer>> futures = new ArrayList<>();
for (int n = 1; n <= 3; n++) { int number = n; // capture for the lambda Future<Integer> f = pool.submit(() -> number * number); // β
each task returns a square futures.add(f); }
for (Future<Integer> f : futures) { System.out.println("Result: " + f.get()); // collect each result in order }
pool.shutdown(); }}A few details worth pointing out.
- We copy
ninto a localnumberbecause a lambda can only use a variable that does not change. - We store every
Futurein a list, so none of them get lost. - The collecting loop calls
get()on each Future in turn, gathering all the squares.
Output
Result: 1Result: 4Result: 9This βsubmit all, then collect allβ shape is so common that Java gives you a shortcut.
π οΈ invokeAll for a list of Callables
When you have a list of Callables and want to run them all and wait, invokeAll does it in one call. You pass a list of Callables and get back a list of Futures, one per task. By the time it returns, every task is finished, so each get() returns instantly.
This program builds three tasks and runs them all with one method call.
import java.util.List;import java.util.concurrent.Callable;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Future;
public class Main { public static void main(String[] args) throws Exception { ExecutorService pool = Executors.newFixedThreadPool(3);
List<Callable<String>> tasks = List.of( () -> "Alex finished", () -> "Riya finished", () -> "Order packed" );
List<Future<String>> results = pool.invokeAll(tasks); // β
runs all, waits for all
for (Future<String> f : results) { System.out.println(f.get()); // every task is already done here }
pool.shutdown(); }}What invokeAll is doing for you.
- It submits all the tasks at once and runs them across the pool.
- It blocks until every task has finished, then returns the list of Futures.
- Because all tasks are done, the
get()calls in the loop never have to wait.
Output
Alex finishedRiya finishedOrder packedSo invokeAll is the tidy choice for a fixed batch of work when you want all the answers before moving on.
π€ What happens when a task fails
Real tasks can fail. A network call times out, a file is missing, a number is divided by zero. With Callable, the failure rides back to you through the Future. When the task throws, future.get() throws an ExecutionException, a wrapper whose βcauseβ is the real error. You unwrap it with getCause().
This task divides by zero on purpose, and get() surfaces that failure.
import java.util.concurrent.*;
public class Main { public static void main(String[] args) { ExecutorService pool = Executors.newFixedThreadPool(1);
Future<Integer> future = pool.submit(() -> 10 / 0); // β throws inside the task
try { Integer result = future.get(); System.out.println("Result: " + result); } catch (ExecutionException e) { System.out.println("Task failed: " + e.getCause()); // β
the real error } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
pool.shutdown(); }}Two exceptions can come out of get():
ExecutionExceptionmeans the task itself threw something. Reade.getCause()for the actual error.InterruptedExceptionmeans the waiting thread was interrupted while blocked. Restoring the interrupt withThread.currentThread().interrupt()is the polite, correct response.
Output
Task failed: java.lang.ArithmeticException: / by zeroErrors in background work do not vanish. They wait in the Future until you call get(), then speak up. Handle them and your program stays in control.
βοΈ Callable vs Runnable
Both are tasks for threads, but they differ in two clear ways:
- Runnable has
run(), returns nothing (void), and cannot throw checked exceptions. Use it for fire-and-forget work, like logging or saving a file. - Callable has
call(), returns a value, and can throw checked exceptions. Use it with a Future when the task must give something back, like a computed total.
Settle this before you write the task.
π A peek at CompletableFuture
CompletableFuture is a richer future that lets you chain steps and react when a result arrives, without ever blocking on get(). You attach a follow-up action that runs automatically once the value is ready. For larger programs that string many async steps together, it is usually the better fit. Plain Callable and Future stay the simple choice for βrun this and fetch the answer.β
β οΈ Common Mistakes
A few Callable and Future slip-ups trip up almost everyone at first.
- Calling
get()too early. It blocks until the result is ready, so calling it right after submit throws away your parallelism.
Future<Integer> future = pool.submit(task);Integer result = future.get(); // β blocks straight away, no work done in parallel// ... do nothing useful while waiting ...Future<Integer> future = pool.submit(task);doOtherWork(); // β
run other work while the task runsInteger result = future.get(); // β
now collect, with the wait already shortened- Not handling ExecutionException. When the task fails,
get()throwsExecutionException. Catching only the wrong type, or ignoring it, hides real failures.
Integer result = future.get(); // β unhandled checked exceptions, fails to compile or hides errorstry { Integer result = future.get();} catch (ExecutionException e) { System.out.println("Task failed: " + e.getCause()); // β
read the real cause} catch (InterruptedException e) { Thread.currentThread().interrupt(); // β
restore the interrupt flag}- Forgetting to shut down the pool. A Callable still runs on an executor. If you never call
shutdown, the poolβs threads keep the program alive and it may never exit.
ExecutorService pool = Executors.newFixedThreadPool(2);pool.submit(task);// β no shutdown, so the program can hang at the endExecutorService pool = Executors.newFixedThreadPool(2);pool.submit(task);pool.shutdown(); // β
pool can finish and the program can exitβ Best Practices
Habits that keep your Callable and Future code clean and fast.
- Use Callable when a task returns a value. Use Runnable when it does not. Pick this before you write the task body.
- Submit, then do other work, then
get(). Callget()only when you truly need the result, so the background work overlaps with your main thread. - Check
isDone()if you do not want to wait. It tells you whether the result is ready without blocking, which is handy in a loop. - Use a timeout with
get(timeout, unit). A slow task should not freeze your program forever. A bounded wait keeps you in control. - Always handle
ExecutionException. Reade.getCause()so the real failure is visible, not swallowed. - Still shut down the pool. Callable runs on an executor, which you must shut down so the program can end cleanly.
π§© What Youβve Learned
Great, that completes the Multithreading module. Letβs recap Callable and Future.
- β
Callable is a task that returns a value (via
call) and can throw checked exceptions, unlike Runnable which returns nothing. - β Submitting a Callable to an executor gives back a Future, a claim ticket for the not-yet-ready result.
- β
future.get()fetches the result, blocking until it is ready, and throwsExecutionExceptionif the task failed. - β
Submit the task, do other work, then call
get()to make the most of parallelism. - β
invokeAllruns a whole list of Callables and waits for all of them, andCompletableFutureis the modern, non-blocking alternative.
Check Your Knowledge
Test what you learned. Pick an answer for each question, then click Check.
- 1
How does Callable differ from Runnable?
Why: Callable returns a result; Runnable's run is void and returns nothing.
- 2
What does submitting a Callable to an executor return?
Why: submit returns a Future immediately, which will hold the result once the task finishes.
- 3
What does future.get() do?
Why: get() blocks the calling thread until the result is available, then returns it.
- 4
When should you call future.get()?
Why: Since get() blocks, do other work first and call it only when you need the result, to keep parallelism.
π Whatβs Next?
You now know how to run tasks that return a result. Next we look at a richer, non-blocking way to chain async steps together so you never have to sit and wait on get().