Java / Advanced / Lesson 14
Executors, futures, and cancellation
Design bounded asynchronous work with explicit ownership, result handling, interruption policy, and shutdown. Keep Java 17 lifecycle requirements visible.
On this page
Lesson goals
- Separate task submission from task completion.
- Bound queues and observe asynchronous failures.
- Define cooperative cancellation and executor shutdown.
Budget tasks, workers, and downstream capacity
An executor separates task submission from execution policy. A fixed number of workers does not necessarily imply a bounded queue: some convenience factories use an unbounded queue, allowing memory and latency to grow under overload. Choose task admission, queue capacity, rejection behavior, and downstream limits from the service budget. A larger pool cannot create more database or network capacity.
CPU-bound work generally needs different parallelism from mostly blocking I/O. Avoid creating an executor per request, and avoid silently sharing a common pool with unrelated blocking workloads. Assign a clear lifecycle owner. In Java 17, ExecutorService is not AutoCloseable, so do not copy a newer try-with-resources executor example into the baseline without checking its API version.
Observe a submitted task through its Future
submit returns a Future that represents completion or failure. get waits for the result and can surface ExecutionException wrapping the task's cause, or InterruptedException if the waiting thread is interrupted. A returned Future is not proof that the task succeeded. The example submits a pure calculation and prints 9 only after get obtains the result.
The finite teaching workload shuts the executor down in finally and waits for termination. shutdown rejects new work while allowing accepted tasks to finish; shutdownNow attempts interruption and returns work that had not started, but it cannot forcibly make arbitrary code stop safely. A real shutdown policy needs a deadline and a response when tasks ignore interruption or remain blocked in an unsuitable API.
public class Main {
public static void main(String[] args) throws Exception {
var pool=java.util.concurrent.Executors.newFixedThreadPool(2);
try {
var result=pool.submit(()->3*3);
System.out.println(result.get());
} finally {
pool.shutdown();
if(!pool.awaitTermination(2,java.util.concurrent.TimeUnit.SECONDS)) pool.shutdownNow();
}
}
}
9
The result is retrieved before shutdown. The tiny task has no external blocking dependency, and the example leaves no owned worker pool running.
Compose results without hidden blocking
CompletableFuture supports result transformations and combinations. thenApply maps a successful value, thenCompose flattens a dependent asynchronous stage, and thenCombine combines independent stage results. Understand whether a continuation runs on a completing thread or through an asynchronous executor variant; adding Async changes scheduling policy, not the semantic need to observe failure.
The example uses already-completed stages to isolate composition semantics. Four doubles to eight, then combines with three to produce eleven. No thread-pool behavior is being benchmarked. join propagates exceptional completion through an unchecked wrapper, while get has its checked waiting contract. A chain that is created and ignored can hide both a failed operation and an unfinished request.
public class Main {
public static void main(String[] args) throws Exception {
var result=java.util.concurrent.CompletableFuture.completedFuture(4)
.thenApply(n->n*2)
.thenCombine(java.util.concurrent.CompletableFuture.completedFuture(3),Integer::sum);
System.out.println(result.join());
}
}
11
The stages are already complete, so this demonstrates value flow rather than claiming parallel execution or speedup.
Treat interruption as a cooperative protocol
Cancellation requests and interruption are cooperative. Future.cancel(true) may request interruption, but task code must respond appropriately and release owned resources. When catching InterruptedException, either propagate it through an allowed contract or restore the interrupt flag before returning when propagation is not possible. Swallowing it and continuing indefinitely defeats shutdown and request deadlines.
Do not use Thread.stop to force termination. Design loops with interruption checks and use interruptible blocking operations where appropriate. A timeout while waiting for a result does not automatically cancel the task or undo its side effects. Define whether the caller requests cancellation, how the worker acknowledges it, and how an uncertain external outcome is reconciled.
Test saturation and exit behavior
Test a successful task, a failed task, cancellation while waiting, queue saturation, and application shutdown. Verify failures are observed and owned workers terminate under the documented bound. Use latches or controlled test collaborators rather than sleeps to stage overlap, and keep an outer test timeout so a defect does not hang the suite forever.
Avoid synchronously waiting inside a saturated pool for work that needs the same pool to run. This can create starvation or deadlock even without synchronized monitors. Trace dependency edges between tasks and pools alongside resource limits. A good asynchronous design makes ownership, admission, result flow, timeout meaning, and cleanup visible instead of treating background execution as a way to forget work.
Common errors and fixes
ExecutionException
- Why it happens
- A submitted task failed and get exposed its wrapped cause.
- How to fix it
- Inspect the cause and apply the operation's failure policy.
- Prevent it next time
- Observe every submitted task result or failure channel.
RejectedExecutionException
- Why it happens
- The executor is shut down or its configured admission policy rejected work.
- How to fix it
- Apply a defined overload response instead of infinite resubmission.
- Prevent it next time
- Test shutdown and saturation explicitly.
The JVM remains alive after work finishes
- Why it happens
- An owned non-daemon worker pool was not terminated.
- How to fix it
- Shut down the executor and verify termination under a deadline.
- Prevent it next time
- Assign one lifecycle owner and test exit paths.
Check your understanding
1. Does Future.get with a timeout guarantee the task stopped when the timeout expires?
Show worked solution
No. It limits the caller's wait. The task may continue and complete or produce side effects. Define cancellation separately, make it cooperative, and reconcile externally visible outcomes rather than assuming timeout equals rollback.
2. A one-thread pool runs a task that submits another task to the same pool and waits for it. What risk exists?
Show worked solution
The only worker is blocked waiting for queued work that cannot start. Avoid the blocking dependency, compose stages, execute suitable work directly, or redesign the scheduling boundary. Increasing the pool without reasoning about dependencies only hides the flaw temporarily.
Key takeaways
- Submission is not completion, and timeout is not cancellation.
- Executors need admission limits and a lifecycle owner.
- Interruption requires a cooperative cleanup policy.