Retrieving Results from Futures
A Runnable is handed to an ExecutorService with execute, which returns as soon as the task is placed in a queue.
Why Hand Work to a Thread Pool
Suppose a program has several pieces of work it wants to run in parallel. It is possible to create and manage a separate thread for every piece, but that quickly becomes tedious. The ExecutorService approach separates two responsibilities: the main program describes the work, and a pool of threads carries it out. This article answers a practical question. Once the main program has handed a task to the pool, how does it send the task off, and how does it get back an answer when the task produces one?
Handing a Runnable to execute
A Runnable object describes a unit of work. To give such an object to an ExecutorService, the program calls execute on the executor and passes the task in: executor.execute(task). The call does not run the task on the spot in the calling thread. Instead, the task is placed into a queue of tasks waiting to be run.
The method returns immediately once the task has been queued. The caller moves on to its next statement without waiting. The threads in the pool are the ones that take tasks off the queue and execute them. This split is the heart of the design: the code that submits work and the code that performs work run on different threads, and submission is quick because it only involves enqueueing.
- The Runnable describes the work but does not run it
- execute places the task in the queue and returns
- The next line may run before the task has finished
What do you think happens?
Immediately after executor.execute(task) returns, has the task necessarily finished?
Reveal answer
No, because execute returns once the task is queued
execute returns as soon as the task is placed in the queue. The caller then moves on without waiting, and the pool threads take the task off the queue and run it at a later time.
Why a Runnable Cannot Return a Value
A Runnable is fine when a task only performs an action. When a task must return a result, the ExecutorService framework offers a different type: Callable<T>. It is a parameterized functional interface. It declares a single method named call, which takes no parameters and returns a value of type T. The type parameter T names whatever kind of value the task produces, so a Callable<Integer> returns an integer and a Callable<String> returns a string.
| Feature | Runnable | Callable<T> |
|---|---|---|
| Purpose | Describes a unit of action | Describes a unit of computation |
| Method | Performs its work with no value handed back | Declares call, which takes no parameters and returns a value of type T |
| Value produced | Nothing is handed back | A value of type T, such as an integer or a string |
| Submitted with | execute or submit | submit |
| Caller receives | No result to retrieve | A Future<T> that stands in for the value |
Runnable and Callable compared. The return value is the key difference.
The contrast with Runnable is the return value. A Runnable task is a unit of action with nothing handed back, while a Callable task is a unit of computation that yields something. That difference is what makes a separate mechanism for retrieving results necessary.
Submitting a Callable and Receiving a Future
A Callable object c is submitted with executor.submit(c). Like execute, the Callable is not run in the calling thread at the moment of submission. It will be executed at some later time. This creates an obvious question: once the computation completes, how does the program get its answer?
The answer is a second interface, Future<T>. A Future<T> stands in for a value of type T that may not be available yet. The call executor.submit(c) returns such a Future, and the Future represents the result of the computation that is still under way. The program holds onto this placeholder and asks it for the value when it needs the value.
The diagram shows two paths that start at the moment of submission. One path leads from the main program to the Future, which the main program keeps in hand. The other leads to a pool thread that runs call() at some later time. The value produced by call() reaches the main program only when the main program asks the Future for it. Read the figure as a relay: the Future is the handle that connects the program's present-time request to the pool's later-time computation.
Checking Progress with isDone
A Future defines several methods. Two of them matter most for retrieving results. The first is isDone(), a boolean-valued method that reports whether the result is available. The second is get(), which retrieves the value the Future holds.
isDone concerns one transition: before the computation completes, the Future reports that its result is not yet available, and after completion it reports that the result is available. Calling isDone is a question the program can ask at any time without changing the Future's state. It simply reads the current status and never waits.
- submit returns a Future before the value exists
isDoneanswers a yes or no question without blocking- Both branches can be reached depending on timing
Calling get and Waiting for the Value
The get method behaves differently from isDone. Calling v.get() retrieves the value, and if the value is not yet available, the call blocks until it is. Blocking means the calling thread pauses at that line and resumes only when the result arrives. Because of this, get is the natural way to wait for an answer, but calling it early will stall the program.
Submitting a Callable and reading its value
A program submits a Callable<Integer> that computes a total, then needs that total before printing it. Trace what happens to the calling thread.
- 1Submit the task
executor.submit(c)queues the Callable and returns a Future<Integer> right away. The main program holds the Future, not the total. - 2Check progress if useful
isDone()can report whether the total is ready yet. It answers immediately and does not change the Future. - 3Ask for the value
get()is called. Ifcall()has not finished, the calling thread blocks at this line. - 4Resume with the result
When the pool thread finishes
call(), the value reaches the Future,get()returns it, and the calling thread continues to the print statement.
The main program never receives the total from submit itself. It receives a Future, and it obtains the integer only through get, which may pause the calling thread until the value is ready.
- 1The lambda is a Callable<Integer> because it returns an integer value.
- 2submit queues the task and returns a Future<Integer> before the product is computed.
- 3get is called inside try, because it can raise exceptions.
- 4If the result is not ready, the line blocks until the pool thread finishes
call().
Mistakes with Futures and Tasks
Common mistake Expecting execute to return a value
What it looks likeint total = executor.execute(c);Why it failsexecute accepts a Runnable and returns as soon as the task is queued. A Runnable hands back no value, so there is nothing to assign.
Do this insteadUse a Callable<T> and submit it, then read the value through the Future with get.
Common mistake Assuming submit runs the task in the calling thread
What it looks likeexecutor.submit(c); System.out.println("done");Why it failssubmit does not run the Callable at the moment of submission. The task runs later on a pool thread.
Do this insteadTreat the returned Future as the way to learn the outcome, and call get when the value is needed.
Common mistake Calling get right after submit without intending to wait
Why it failsget blocks until the value is available, so the calling thread pauses at that line.
Do this insteadCheck isDone first, or call get at the point where the answer is genuinely required.
Common mistake Leaving get outside a try..catch block
Why it failsget can raise exceptions, so an unhandled call can fail the program.
Do this insteadWrap the call in try..catch and handle the exceptions it can raise.
Practice: Trace the Calls
A program calls executor.execute(r) with a Runnable r, then immediately calls executor.submit(c) with a Callable<String> c, then calls get on the returned Future. Trace where each step happens: which calls return immediately, which thread runs r and c, and at which point the main thread may be paused.
Hints
- Ask what execute returns once the task is queued.
- Ask what submit returns and whether the Callable has finished.
- Ask when get blocks the calling thread.
Key Takeaways
- A Runnable is handed to an ExecutorService with execute, which returns as soon as the task is placed in a queue, so the caller does not wait for the task to finish.
- A Callable<T> declares call, takes no parameters, and returns a value of type T, which is why it can produce a result a Runnable cannot.
submit(c)returns a Future<T> that stands in for a value that may not exist yet, and the Callable runs later on a pool thread.isDone()reports whether the result is available without waiting, whileget()retrieves the value and blocks until it is available, so it must be wrapped in try..catch.