skip to content

What does ScheduledFuture give you, and how do you cancel, query, or read the result of scheduled tasks?

level: middleimportance: should knowfreq 48%

answer

  1. ScheduledFuture = Future + Delayed
  2. get() blocks + rethrows as ExecutionException
  3. cancel(true) interrupts running; cancel stops future periodic runs
  4. getDelay() = time to next run
  5. periodic Future never done on its own -> use it to cancel

basics

~20 s

Every schedule call returns a ScheduledFuture. For a one-shot Callable you read its result with get(). For any task you can cancel it with cancel(mayInterruptIfRunning), and check getDelay() to see how long until the next run. Keep the reference if you ever want to stop the task.

solid answer

~50 s

All four scheduling methods return a ScheduledFuture<?> (a Future plus a Delayed). For a one-shot schedule(Callable, …) you call get() to block for and retrieve the result, or get(timeout, unit) to bound the wait; get() rethrows any task exception wrapped in an ExecutionException. cancel(boolean mayInterruptIfRunning) stops the task: for a periodic task it prevents all future runs (this is how you deliberately stop a recurring job), and if mayInterruptIfRunning is true and a run is in progress, the worker thread is interrupted. isCancelled() and isDone() report state; getDelay(unit) (from Delayed) tells you how long until the next scheduled execution. A subtle point: for periodic tasks a healthy Future never becomes 'done' on its own, so get() would block forever — you use the Future mainly to cancel, not to read a result. Keeping the returned handle is the only way to later stop a fire-and-forget periodic task short of shutting the whole pool down.

code

java · 8 lines
java
// one-shot result
ScheduledFuture<Integer> f = exec.schedule(() -> 21 * 2, 1, TimeUnit.SECONDS);
int answer = f.get(5, TimeUnit.SECONDS);   // bounded wait, throws on task error

// stop a recurring job without killing the pool
ScheduledFuture<?> job = exec.scheduleAtFixedRate(this::tick, 0, 1, TimeUnit.SECONDS);
job.cancel(true);   // interrupt an in-progress tick and prevent all future runs
System.out.println(job.isCancelled()); // true

go deeper

for a junior

Knows schedule returns a ScheduledFuture and that you can cancel a task with it.

for a middle

Uses get() for one-shot Callable results, cancel(true/false) to stop tasks, and getDelay()/isCancelled() to inspect state; keeps the handle to stop a periodic job.

for a senior

Explains the Future-vs-Delayed split, why periodic Futures never complete, interrupt semantics of cancel(true), and how task exceptions surface via ExecutionException from get().

for a principal

Designs lifecycle/cancellation policy for many scheduled jobs (handle registries, graceful vs. forceful stop, interrupt-responsive task design) and how it interacts with pool shutdown and resource cleanup.

## What you get back Every `schedule*` call hands you a `ScheduledFuture<V>`. This interface combines two contracts: - **`Future<V>`** — a handle to a result that may not exist yet: `get()`, `get(timeout, unit)`, `cancel(...)`, `isCancelled()`, `isDone()`. - **`Delayed`** — `getDelay(TimeUnit unit)`: how much time remains before the task is due to run next (negative if overdue). For `schedule(Runnable, …)` and the two periodic methods, `V` is effectively `Void` (the Future yields `null`). For `schedule(Callable<V>, …)`, `V` is the value your `call()` returns. ## Reading a result (one-shot Callable) ```java ScheduledFuture<Integer> f = exec.schedule(() -> compute(), 2, TimeUnit.SECONDS); Integer result = f.get(); // blocks until the task has run, then returns its value ``` - `get()` **blocks** the calling thread until the task finishes, then returns the value. - If the task threw, `get()` throws `ExecutionException` whose `getCause()` is your original exception. (This is also how you'd discover a one-shot failure.) - `get(timeout, unit)` throws `TimeoutException` if the result is not ready in time, so you do not block forever. ## Cancelling a task ```java ScheduledFuture<?> handle = exec.scheduleAtFixedRate(job, 0, 1, TimeUnit.SECONDS); // later... handle.cancel(false); // stop future runs; let an in-progress run finish ``` - `cancel(false)` — if the task has **not started**, it will not run; if it is a periodic task, **no further executions** occur. An in-progress run is allowed to finish. - `cancel(true)` — additionally **interrupts** the worker thread if a run is currently executing, so a task that honors interruption (checks `Thread.interrupted()` or calls interruptible blocking methods) can stop early. - For a periodic task, `cancel(...)` is the normal way to **stop just that recurring job** without shutting down the whole pool. - After cancellation, `isCancelled()` returns `true` and `isDone()` returns `true`. ## Querying state and timing - `isDone()` — true once the task completed, was cancelled, or threw. **A still-running periodic task is *not* done.** - `isCancelled()` — true only if it was cancelled before completing normally. - `getDelay(unit)` — time until the next run; lets you build delay-aware logic or sort by readiness (`ScheduledFuture` is `Comparable`). ## The periodic-task subtlety For `scheduleAtFixedRate`/`scheduleWithFixedDelay`, a healthy task keeps recurring forever, so its Future **never completes on its own**. Calling `get()` on it would block until the task is cancelled or throws. So for periodic tasks you treat the Future as a **remote control** (cancel/inspect), not as a result holder. The only ways a periodic Future 'finishes' are: you cancel it, or the task throws an uncaught exception (which silently cancels it — see the dedicated failure question). ## Practical pattern Store handles you might need to stop: ```java private final List<ScheduledFuture<?>> jobs = new ArrayList<>(); ... jobs.add(exec.scheduleWithFixedDelay(this::sync, 0, 30, TimeUnit.SECONDS)); ... void stopJobs() { jobs.forEach(f -> f.cancel(false)); } ``` Without the handle, the only way to stop a fire-and-forget periodic task is `shutdownNow()` on the whole executor.

  • What is the difference between cancel(true) and cancel(false)?
    Both prevent a not-yet-started run (and all future runs of a periodic task). cancel(true) additionally interrupts the worker thread if a run is currently executing, so an interruptible task can abort mid-run; cancel(false) lets the in-progress run finish naturally.
  • Why does get() on a periodic task's Future tend to hang?
    A healthy periodic task never completes — it just keeps recurring — so its Future never becomes done. get() blocks until completion, which only happens if you cancel it or it throws. For periodic tasks you use the Future to cancel/inspect, not to read a result.

saying these in an interview costs you the question

  • Thinking get() returns the result of every periodic iteration (a periodic Future yields no value and never completes normally).
  • Believing cancel() stops the entire executor (it stops only that task; use shutdown() for the pool).
  • Assuming cancel(true) always halts a running task — it only interrupts; the task must respond to interruption.
  • Treating isDone() as true while a periodic task is still recurring (it is false until cancelled or thrown).

context