Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

You do not wait for FutureTask.cancel() itself: it is a synchronous call. To wait until the FutureTask reaches a terminal state, call get() and handle the expected CancellationException. That confirms the future is canceled; it does not necessarily mean the task’s code has stopped running. cancel(true) requests interruption, which the task must cooperate with.

The basic cancel-and-wait pattern

For an unbounded wait for the future’s state, cancel it and then call get():

task.cancel(true);

try {
    task.get();
} catch (CancellationException expected) {
    // The FutureTask reached its canceled state.
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    // This waiting thread was interrupted.
} catch (ExecutionException e) {
    // The computation failed rather than completing through cancellation.
}

A canceled future does not return a value from get(); it reports cancellation by throwing CancellationException. If cancellation loses a race with successful completion, get() can return the result instead. If the computation failed first, it throws ExecutionException.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The key distinction is between FutureTask completion and the end of the task body. A successful cancel(true) can mark the future canceled and request an interrupt while the callable is still running cleanup—or continues to run because it ignores interruption. Consequently, get() after cancellation is not proof that arbitrary task code has physically stopped.

What cancel(false) and cancel(true) mean

  • cancel(false) attempts to prevent a task that has not started from running. If it is already running, no interrupt is requested, so its code may continue even if the future becomes canceled.
  • cancel(true) also attempts to interrupt the thread executing the task, if it is running. Interruption is a request, not forced termination. A task that ignores the interrupt, suppresses InterruptedException, or is stuck in a non-interruptible operation may continue.

The boolean returned by cancel reports whether that cancellation attempt succeeded. If the task has already completed or another cancellation won the race, the call may return false. Check isCancelled() when you need the future’s cancellation status; do not infer the final state solely from the return value at one call site. The FutureTask API documents these methods and their completion behavior.

Why get() is better than polling

get() is the blocking wait supplied by the Future abstraction. By contrast, isDone() is a nonblocking status check: it returns true after normal completion, exceptional completion, or cancellation. It does not tell you that a result is available or that cancellation specifically occurred.

// Usually avoid this as a waiting strategy:
while (!task.isDone()) {
    Thread.sleep(10);
}

Polling adds arbitrary latency and wakeups, requires its own interruption handling, and still requires checking how the future completed. Use get() for an indefinite wait, or timed get when the caller has a deadline. Do not use a fixed sleep as a synchronization protocol.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Handle interruption in both threads

There can be two separate interrupt concerns: the worker receiving the cancellation request, and the caller waiting in get(). If the waiting thread is interrupted, get() throws InterruptedException. If the method cannot propagate that exception, restore the interrupt status with Thread.currentThread().interrupt(), as in the example above; do not silently swallow it.

The task should also cooperate with interruption. For example, a loop can check the interrupt flag and release resources in a finally block:

FutureTask<Void> task = new FutureTask<>(() -> {
    try {
        while (!Thread.currentThread().isInterrupted()) {
            doSmallUnitOfWork();
        }
    } finally {
        releaseResources();
    }
    return null;
});

For interruptible blocking calls, handle InterruptedException by exiting or propagating the cancellation signal. If you catch it and continue without a deliberate policy, the task may keep running after its future has been canceled:

try {
    blockingOperation();
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    return;
}

Java interruption is cooperative; it does not safely kill a thread. Do not use deprecated forced-stop techniques as a substitute for designing a task that can stop and clean up.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

If you need proof that the task body stopped

Have the task publish its own termination acknowledgment, normally from a finally block. A latch lets the caller separately wait for that signal:

CountDownLatch stopped = new CountDownLatch(1);

FutureTask<Void> task = new FutureTask<>(() -> {
    try {
        while (!Thread.currentThread().isInterrupted()) {
            doWork();
        }
    } finally {
        stopped.countDown();
    }
    return null;
});

executor.execute(task);

// Later:
task.cancel(true);
try {
    task.get();
} catch (CancellationException expected) {
    // FutureTask cancellation observed.
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    return;
} catch (ExecutionException e) {
    throw new RuntimeException(e);
}

if (!stopped.await(5, TimeUnit.SECONDS)) {
    throw new TimeoutException("Task did not acknowledge cancellation");
}

Import CountDownLatch, TimeUnit, and the relevant exception types from java.util.concurrent. The latch must be released in a finally block, and the task must reach that block. If it ignores interruption or remains stuck, the acknowledgment may never arrive; the timeout detects that condition but cannot force the task to stop.

If your application directly owns the worker Thread, join() can wait for that specific thread’s termination after requesting cancellation. This is not a general way to get the worker thread from an arbitrary executor. For a whole executor, use its shutdown and termination APIs rather than treating one future as a proxy for every worker.

Timed waits and cancellation races

Use get(timeout, unit) to bound how long the caller waits:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
task.cancel(true);

try {
    task.get(5, TimeUnit.SECONDS);
} catch (CancellationException expected) {
    // The future is canceled.
} catch (TimeoutException e) {
    // The caller's wait expired; this did not stop the task.
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
} catch (ExecutionException e) {
    // The computation failed.
}

A timeout means only that no terminal result was observed within the allotted wait. It does not cancel the task. If your policy is to request cancellation after a wait expires, call cancel(true) explicitly—and remember that interruption still depends on task cooperation.

Completion and cancellation can race. The task may finish normally or exceptionally just before cancellation takes effect; another caller may cancel it first. Treat the future’s state and get() outcome as authoritative:

  • If cancellation wins, isCancelled() is true and get() throws CancellationException.
  • If normal completion wins, get() returns the result.
  • If exceptional completion wins, get() throws ExecutionException.
  • isDone() is true in all three terminal cases, so it does not identify which occurred.

For the full contract, see the Java 26 Future API and the FutureTask API.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

done() is not task-body acknowledgment

A FutureTask subclass can override done() for notification or bookkeeping:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
@Override
protected void done() {
    completionSignal.countDown();
}

That hook corresponds to the FutureTask entering its done state, including cancellation. It should not be treated as proof that user code has ceased executing after an interrupt-based cancellation. Put a task-body acknowledgment in the callable’s finally block when that stronger guarantee matters.

Using an executor or a different future type

For ordinary application code, ExecutorService.submit(...) returns a Future; program to that interface unless you specifically need FutureTask features. The same basic cancellation and waiting pattern applies:

Future<Result> future = executor.submit(callable);
future.cancel(true);

try {
    Result result = future.get();
} catch (CancellationException expected) {
    // Canceled.
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
} catch (ExecutionException e) {
    // Failed.
}

If you need to wait for the entire executor to stop accepting and finish work, use executor lifecycle methods:

executor.shutdown();

if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
    executor.shutdownNow();

    if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
        throw new IllegalStateException("Executor did not terminate");
    }
}

shutdownNow() requests interruption of active tasks; it does not guarantee termination for tasks that ignore interrupts. This is a different scope from waiting for one future.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

CompletableFuture has a different cancellation model: cancellation completes that future exceptionally but does not provide the same direct control over the computation that might have produced its result. Do not substitute it when the requirement is specifically to interrupt a running task. A completed FutureTask is not normally reusable for another computation; create a new task instead.

Quick decision guide

What you need Use Limitation
Wait for the FutureTask’s terminal state get(), catching CancellationException Does not prove task code has stopped
Request cooperative stop of running work cancel(true) and interruption-aware task code Task must honor interruption
Bound how long the caller waits get(timeout, unit) Timeout does not cancel the task
Wait for task cleanup or body exit Task-owned latch or completion signal from finally Signal may time out if the task does not exit
Wait for one directly managed thread Thread.join() Requires ownership of that thread
Wait for executor termination awaitTermination(...) Applies to the executor, not just one task

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.