To cancel a task in ExecutorService, we call cancel() on the Future that submit() returned, so a task that still waits in the queue never runs and cancel(true) interrupts a task that is already running. A completed task cannot be cancelled, and cancel() returns false for it.
We cancel tasks when their result is no longer needed, for example when a user clicks Cancel on a report export, when a page stops waiting for a slow service after a timeout, or when one of several parallel searches has already found the answer.
The following example cancels a task that waits behind a busy task in a single-thread executor. The helper await() is declared in section 2.
CountDownLatch busy = new CountDownLatch(1);
ExecutorService pool = Executors.newSingleThreadExecutor();
pool.submit(() -> await(busy));
Future<String> export = pool.submit(() -> "report.pdf");
boolean cancelled = export.cancel(false); // true
Future.State state = export.state(); // CANCELLED
busy.countDown();
pool.close();
Notice that the export never ran, so cancel(false) was enough. We look at what cancel() does in each task state, how a running task must cooperate with cancel(true), timeouts, cancelled tasks that stay in the queue, shutdownNow() and why CompletableFuture behaves differently.
1. How Future.cancel() Works
The method Future.cancel(boolean mayInterruptIfRunning) asks the executor to stop a task. What it can do depends on where the task is when we call it, and the boolean only matters for a task that has already started.

| Task state | cancel(false) | cancel(true) |
|---|---|---|
| Waiting in the queue | Returns true, the task never runs | Returns true, the task never runs |
| Running | Returns true, the task keeps running and its result is thrown away | Returns true and calls interrupt() on the worker thread |
| Completed, failed or already cancelled | Returns false, nothing changes | Returns false, nothing changes |
After a successful cancel(), the future reports the same state no matter what the task does afterwards.
- The isCancelled() and isDone() methods both return true.
- The state() method, added in Java 19, returns CANCELLED.
- The get() method throws CancellationException, and resultNow() throws IllegalStateException.
A true from cancel() means the future is cancelled, not that the task has stopped. The code of a running task can go on for a long time after the call, as section 3 shows.
2. Cancelling a Task That Is Still in the Queue
A task that has not started is the simplest case to cancel. The executor marks its FutureTask as cancelled, and when a worker takes it from the queue later, the worker skips it. Say a reporting app lets users queue PDF exports, and a user who changes the date range cancels the old export before its turn comes. The old export never touches the database.
The examples use a small helper that waits on a latch and restores the interrupt flag when the wait is interrupted.
static void await(CountDownLatch latch) {
try {
latch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
CountDownLatch gate = new CountDownLatch(1);
ExecutorService exports = Executors.newSingleThreadExecutor();
exports.submit(() -> await(gate));
Future<String> oldExport = exports.submit(() -> "march.pdf");
boolean doneBefore = oldExport.isDone(); // false
boolean wasCancelled = oldExport.cancel(false); // true
boolean doneAfter = oldExport.isDone(); // true
boolean secondTry = oldExport.cancel(false); // false
String file = oldExport.get(); // CancellationException
gate.countDown();
exports.close();
The second cancel() returned false because the task was already cancelled. Code that reads the result must expect CancellationException, which is an unchecked exception, so the compiler does not remind us to catch it.
3. Cancelling a Running Task With cancel(true)
Java cannot stop a running thread from the outside. The old Thread.stop() method throws UnsupportedOperationException since Java 20, as the stopping threads article explains. Instead, cancel(true) calls interrupt() on the worker thread, which sets the thread’s interrupt flag, and the task decides when to stop.
A task reacts to the interrupt in one of two ways.
- A task that runs a loop checks Thread.currentThread().isInterrupted() on every iteration and returns when it is set.
- A task that blocks in BlockingQueue.take(), CountDownLatch.await(), Thread.join() or the sleep method gets an InterruptedException from that call, cleans up and returns.
The following example is an image resize job that works through 100,000 items and checks the flag after each one. The started latch makes sure we cancel while the job is running, and the stopped latch shows that the job ended.
AtomicInteger resized = new AtomicInteger();
CountDownLatch started = new CountDownLatch(1);
CountDownLatch stopped = new CountDownLatch(1);
ExecutorService resizer = Executors.newSingleThreadExecutor();
Future<?> job = resizer.submit(() -> {
started.countDown();
for (int i = 0; i < 100_000 && !Thread.currentThread().isInterrupted(); i++) {
resized.incrementAndGet();
LockSupport.parkNanos(TimeUnit.MICROSECONDS.toNanos(100));
}
stopped.countDown();
});
started.await();
boolean interrupted = job.cancel(true); // true
boolean jobEnded = stopped.await(2, TimeUnit.SECONDS); // true
boolean partial = resized.get() < 100_000; // true
resizer.close();
The job stopped long before item 100,000, and its thread went back to the pool for the next task. The parkNanos() call simulates the real work. It returns early when the thread is interrupted, without an exception, so the loop condition is what ends the job.
With cancel(false), the same running task would not be interrupted. The future is still marked as cancelled, but the task runs to its end and its result is discarded.
CountDownLatch release = new CountDownLatch(1);
CountDownLatch running = new CountDownLatch(1);
CountDownLatch finished = new CountDownLatch(1);
ExecutorService worker = Executors.newSingleThreadExecutor();
Future<?> upload = worker.submit(() -> {
running.countDown();
await(release);
finished.countDown();
});
running.await();
boolean marked = upload.cancel(false); // true
release.countDown();
boolean ranToEnd = finished.await(2, TimeUnit.SECONDS); // true
worker.close();
Use cancel(false) when a task must not be stopped halfway, such as a payment that writes to two systems, and cancel(true) for work that is safe to abandon, such as a search or a resize.
4. Tasks That Ignore the Interrupt
An interrupt is only a flag, so some code never sees it. A task keeps running after cancel(true) when it does one of these things.
- It runs a CPU loop that never checks isInterrupted().
- It catches InterruptedException, ignores it and continues, which also clears the flag.
- It waits to enter a synchronized block, or it blocks in a socket read on a platform thread. Neither reacts to interrupts.
For loops, add the flag check. For InterruptedException, either return or restore the flag with Thread.currentThread().interrupt(), as the helper await() above does. For blocking I/O, set a read timeout on the socket or HTTP client, or close the resource from another thread so the blocked call throws. Virtual threads use the same interrupt flag, with one difference. A virtual thread blocked in a socket read or connect wakes up on an interrupt, and the call closes the socket and throws SocketException.
5. Cancelling a Task After a Timeout
Say a product page asks a recommendation service for related items and waits at most 200 milliseconds. If the service is slow, the page shows a default list and the slow call must not keep a thread busy. We wait with get(timeout) and cancel the task when the time is up.
static String fetchOrDefault(ExecutorService pool, Callable<String> call, String fallback) {
Future<String> future = pool.submit(call);
try {
return future.get(200, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
future.cancel(true);
return fallback;
} catch (ExecutionException e) {
return fallback;
} catch (InterruptedException e) {
future.cancel(true);
Thread.currentThread().interrupt();
return fallback;
}
}
ExecutorService calls = Executors.newFixedThreadPool(2);
String fast = fetchOrDefault(calls, () -> "phone case", "bestsellers"); // "phone case"
Callable<String> stuck = () -> {
new CountDownLatch(1).await();
return "never";
};
String slow = fetchOrDefault(calls, stuck, "bestsellers"); // "bestsellers"
calls.close();
The slow call blocks on a latch that is never released, so get() times out, and cancel(true) interrupts the await(). Without the cancel, the pool thread would stay blocked and close() would wait forever. For a group of tasks, invokeAll(tasks, timeout, unit) does the same and cancels every task that has not finished in time, as the invokeAll() and invokeAny() guide shows.
6. Cancelled Tasks That Stay in the Queue
A cancelled task is not removed from the queue of a ThreadPoolExecutor. It stays there, with its captured data, until a worker takes it and skips it. With thousands of cancelled tasks behind a long queue, that is a memory leak until the queue drains.
CountDownLatch hold = new CountDownLatch(1);
ThreadPoolExecutor tpe = new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS,
new LinkedBlockingQueue<>());
tpe.submit(() -> await(hold));
Future<String> stale = tpe.submit(() -> "stale");
stale.cancel(false);
int beforePurge = tpe.getQueue().size(); // 1
tpe.purge();
int afterPurge = tpe.getQueue().size(); // 0
hold.countDown();
tpe.close();
The method purge() removes all cancelled Future tasks from the queue. A ScheduledThreadPoolExecutor has a setting for the same problem, setRemoveOnCancelPolicy(true), which removes each task at the moment it is cancelled, as the ScheduledExecutorService article shows.
7. Cancelling Every Task With shutdownNow()
To cancel everything an executor holds, we call shutdownNow(). It interrupts all running tasks and removes the waiting ones from the queue, and it returns those waiting tasks as a List<Runnable>. The ExecutorService shutdown article compares it with shutdown() and close().
The returned tasks are not cancelled. Their futures stay in the RUNNING state forever, so a thread that calls get() on one of them blocks forever. We cancel them ourselves.
CountDownLatch never = new CountDownLatch(1);
ExecutorService batch = Executors.newSingleThreadExecutor();
batch.submit(() -> await(never));
Future<String> second = batch.submit(() -> "b");
Future<String> third = batch.submit(() -> "c");
List<Runnable> neverStarted = batch.shutdownNow();
int drained = neverStarted.size(); // 2
Future.State leftOver = second.state(); // RUNNING
for (Runnable task : neverStarted) {
if (task instanceof Future<?> f) {
f.cancel(false);
}
}
Future.State fixed = third.state(); // CANCELLED
batch.close();
8. Why CompletableFuture.cancel() Does Not Interrupt
A CompletableFuture also has a cancel(boolean) method, but its argument has no effect, because a CompletableFuture is not tied to the thread that computes it. The call completes the future with a CancellationException, so dependent stages run their error handling, while the code inside supplyAsync() keeps running.
CountDownLatch inside = new CountDownLatch(1);
CountDownLatch keepGoing = new CountDownLatch(1);
CountDownLatch completed = new CountDownLatch(1);
ExecutorService async = Executors.newSingleThreadExecutor();
CompletableFuture<String> price = CompletableFuture.supplyAsync(() -> {
inside.countDown();
await(keepGoing);
completed.countDown();
return "9.99";
}, async);
inside.await();
boolean priceCancelled = price.cancel(true); // true
keepGoing.countDown();
boolean stillRan = completed.await(2, TimeUnit.SECONDS); // true
async.close();
When the work must stop as well, we submit it to an ExecutorService and keep the plain Future, or check a shared flag inside the task.
9. Task Cancellation FAQs
Cancelling looks like a single method call, which is why its side effects on threads, results and queues surprise people.
9.1. Does Future.cancel(true) stop the thread?
No. It interrupts the thread, and the thread stops only if the task checks the interrupt flag or is blocked in a method that throws InterruptedException. The pool thread itself is never killed; after the task returns, it runs the next task.
9.2. What is the difference between cancel(true) and cancel(false)?
Both cancel a task that has not started yet. For a running task, cancel(true) also interrupts its thread, whereas cancel(false) lets it run to the end and only discards the result.
9.3. What does get() return after a task is cancelled?
Nothing. It throws CancellationException, even if the task ran to its end after a cancel(false). Check isCancelled() or state() first when that exception is not wanted.
9.4. Can we cancel a task that has already completed?
No. The cancel() method returns false, and the future keeps its result or its exception.
9.5. How do we cancel a task submitted with execute()?
We cannot, because execute() returns no Future. Use submit() when a task may need cancelling, or remove a waiting task with ThreadPoolExecutor.remove(Runnable), which works only before the task starts.
10. Conclusion
The Future.cancel() method always cancels a queued task and asks a running task to stop through an interrupt when we pass true. A running task stops only if it checks the interrupt flag or blocks in an interruptible call, so tasks that may be cancelled must be written for it.
A timeout with get() and cancel(true) keeps slow calls from holding threads, purge() clears cancelled tasks from a long queue, and the tasks returned by shutdownNow() need their own cancel(). A CompletableFuture never interrupts, so work that must stop belongs in an ExecutorService task. The rest of the executor API is in our ExecutorService guide, and the Java concurrency overview groups the related tools by task.
11. References
- Future.cancel Javadoc (Java 25)
- FutureTask Javadoc (Java 25)
- ThreadPoolExecutor.purge Javadoc (Java 25)
- CompletableFuture.cancel Javadoc (Java 25)
Happy Learning !!
I want to execute a function periodically until I get response from client as true. After that I need to stop the executor service but to do that call function has to return the boolean value which I return not whether the scheduled job is hit or not How can I do that