1. Thread.interrupt() and cooperative cancellation
In real applications, tasks can be long-running and sometimes even “hang” — when working with networks, files, or external services. A user can cancel an operation, a server can abort request processing, or a global timeout can simply expire. If you don’t know how to cancel tasks correctly, the application will hang, waste resources, and react poorly to external events.
Key idea: cancellation should be cooperative — the task itself must check whether it has been asked to finish and release resources cleanly.
How Thread.interrupt() works
Each thread has an “interrupted” flag. When you call thread.interrupt(), this flag is set to true. The thread is not “killed”; it must check its own status and exit: periodically call Thread.currentThread().isInterrupted() and finish gracefully.
Example:
Thread worker = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// Working...
try {
Thread.sleep(100); // Can be interrupted
} catch (InterruptedException e) {
// The flag is cleared, but we can interrupt ourselves again
Thread.currentThread().interrupt();
break;
}
}
System.out.println("Thread ended due to interruption.");
});
worker.start();
// ... later
worker.interrupt();
Where does the flag work automatically?
- Methods that can block (sleep, wait, join, operations of blocking structures) throw InterruptedException when interrupted.
- In other cases (for example, in a compute loop) you need to check isInterrupted() manually.
Pattern “set the flag — and exit quickly”
- In the caller: thread.interrupt()
- In the task: periodically check Thread.currentThread().isInterrupted()
- If needed — release resources cleanly and finish.
Common mistake: expecting interrupt() to instantly “kill” a thread. No — it’s only a signal; the task must react on its own.
2. Future.cancel(), CancellationException and task cancellation
How Future.cancel works
When you run a task via ExecutorService.submit(), you get a Future. It has the method cancel(boolean mayInterruptIfRunning):
- If the task hasn’t started yet — it won’t be launched.
- If the task is already running and mayInterruptIfRunning == true — interrupt() is called on the thread executing the task.
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<?> future = executor.submit(() -> {
while (!Thread.currentThread().isInterrupted()) {
// Long-running work
}
System.out.println("Task finished due to cancellation.");
});
// ... later
future.cancel(true); // Request cancellation of the task
What actually happens to the task
Cancellation via Future is not a magic “kill the thread” button; it is essentially a polite form of Thread.interrupt(). If the task correctly checks the interruption flag, it will terminate gracefully. If not, it will continue until natural completion.
If you call future.get() after cancellation, you’ll get a CancellationException — a reminder that the task was withdrawn.
3. CompletableFuture: cancellation, timeouts, and chains
Canceling a CompletableFuture
CompletableFuture also has cancel(boolean). If the task hasn’t completed yet, it will be canceled, and all subsequent handlers (thenApply, thenAccept, etc.) won’t be invoked.
CompletableFuture<Void> cf = CompletableFuture.runAsync(() -> {
while (!Thread.currentThread().isInterrupted()) {
// Working...
}
System.out.println("CF finished due to cancellation.");
});
// ... later
cf.cancel(true);
Timeouts: orTimeout and completeOnTimeout
- orTimeout(timeout, unit) — completes the CompletableFuture with TimeoutException if it doesn’t finish in time.
- completeOnTimeout(value, timeout, unit) — completes with the given value if it doesn’t finish in time.
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
try { Thread.sleep(5000); } catch (InterruptedException e) {}
return "OK";
});
cf.orTimeout(2, TimeUnit.SECONDS)
.exceptionally(ex -> "TIMEOUT")
.thenAccept(System.out::println); // After 2 seconds: "TIMEOUT"
Propagating cancellation in chains
If you cancel the “top” CompletableFuture, all subsequent steps in the chain won’t be invoked. But when using thenCompose to launch inner asynchronous operations, cancellation is not propagated “up” automatically — you need to design it explicitly (check status, cancel child tasks, use a shared deadline).
Be careful with thenCompose and a custom Executor! Make sure the inner tasks can react to interruption/cancellation and/or receive a shared timeout.
4. StructuredTaskScope: canceling a group of tasks
Structured concurrency and cancellation
StructuredTaskScope (Java 21+) lets you launch a group of tasks and manage their lifecycle as a single unit. If one task fails or a timeout expires, the remaining tasks are automatically canceled.
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> f1 = scope.fork(() -> fetchData1());
Future<String> f2 = scope.fork(() -> fetchData2());
scope.join(); // wait for all tasks to finish
scope.throwIfFailed(); // if at least one failed — throw an exception
String result = f1.resultNow() + f2.resultNow();
System.out.println(result);
}
- If any task fails, the scope cancels all the other tasks.
- If a timeout expires (via scope.joinUntil(deadline)), the scope cancels all tasks.
Completion policies
- ShutdownOnFailure — cancels all tasks on the first failure.
- ShutdownOnSuccess — cancels the remaining tasks as soon as one completes successfully.
5. Practice: safe cancellation of long operations
Example: canceling blocking I/O
If a task blocks on reading from a file or the network, interrupting the thread doesn’t always help — some I/O operations don’t react to interrupt. In modern APIs (NIO, AsynchronousFileChannel) interruption support is better, but still not universal.
Recommendations:
- Use non-blocking I/O when you need cancellation.
- For blocking I/O — set timeouts at the API level (for example, Socket.setSoTimeout).
- For asynchronous tasks — use Future.cancel and react correctly to interruption.
Example: canceling queue/barrier waits
Many synchronizers (BlockingQueue.take(), CountDownLatch.await(), CyclicBarrier.await()) throw InterruptedException when interrupted. In the handler, catch the exception, restore the flag if necessary, and finish the task cleanly.
6. Pattern “time budget”: a shared deadline for a group of operations
In complex applications you often need to set a shared timeout for a group of operations. For example, if a user will wait no longer than 2 seconds, and inside you need to make 3 network calls, they all must fit into the shared deadline.
How to propagate the deadline down the stack?
- Pass a deadline object (for example, Instant deadline) to all potentially blocking methods.
- In each method compute the remaining time: Duration.between(Instant.now(), deadline).
- Use this time for timeouts in blocking operations (await(timeout), poll(timeout), orTimeout(timeout)).
Instant deadline = Instant.now().plusSeconds(2);
void doWork(Instant deadline) throws TimeoutException, InterruptedException {
Duration left = Duration.between(Instant.now(), deadline);
if (left.isNegative() || left.isZero()) throw new TimeoutException();
// Use left for the timeout
queue.poll(left.toMillis(), TimeUnit.MILLISECONDS);
}
Scoped Values / context
In Java 21+ you can use Scoped Values to pass the deadline through the call stack without passing it explicitly to every method.
7. Structured concurrency: canceling the entire scope on failure/timeout
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> f1 = scope.fork(() -> fetchData1());
Future<String> f2 = scope.fork(() -> fetchData2());
boolean completed = scope.joinUntil(Instant.now().plusSeconds(2));
if (!completed) {
scope.shutdown();
throw new TimeoutException("Deadline expired!");
}
scope.throwIfFailed();
// ...
}
- If the deadline expires — the scope cancels all tasks.
- If one task fails — the others are canceled automatically.
8. Common mistakes when working with cancellation and timeouts
Mistake #1: Expecting that interrupt() will immediately terminate the thread. In reality, it’s only a signal — the task must check the status and shut down properly.
Mistake #2: Not checking isInterrupted() in long loops. If you don’t check the interruption flag, the task will run forever even if it was asked to finish.
Mistake #3: Future.cancel() does not lead to cancellation if the task doesn’t react to interrupt. If a task is “deaf”, cancel() won’t help.
Mistake #4: Timeouts are not propagated down the stack. If you don’t pass the deadline to all methods, an inner operation may “hang” longer than it should.
Mistake #5: In thenCompose chains of CompletableFuture cancellation is not propagated automatically. If you cancel the “top” future, inner tasks may continue running — handle cancellation explicitly.
Mistake #6: StructuredTaskScope is not closed (no try‑with‑resources). If you don’t close the scope, child tasks may remain “hanging”.
GO TO FULL VERSION