skip to content

ScheduledExecutorService

Scheduling delayed and repeating tasks, where the real question is scheduleAtFixedRate versus scheduleWithFixedDelay — period from start versus gap from finish. The other detail interviewers like is that one uncaught exception silently kills all future runs.

part ofJavaoverview, primer and where to startread it →
on this pageshow

questions

5

What is a ScheduledExecutorService and how do you use it to run a task after a delay or on a repeating schedule?

level: juniorimportance: must knowfreq 62%

answer

  1. extends ExecutorService + time
  2. schedule / atFixedRate / withFixedDelay
  3. Executors.newScheduledThreadPool
  4. TimeUnit on every call
  5. must shutdown() — replaces java.util.Timer

basics

~10 s

It is an executor that runs tasks later or repeatedly. You create one with Executors.newScheduledThreadPool(n), then call schedule() for a one-shot delay, or scheduleAtFixedRate/scheduleWithFixedDelay to repeat. Always shut it down when done.

solid answer

~40 s

ScheduledExecutorService is a sub-interface of ExecutorService that adds time-based task submission backed by a thread pool. You obtain one via Executors.newScheduledThreadPool(corePoolSize) (or newSingleThreadScheduledExecutor()). It offers four scheduling methods: schedule(Runnable/Callable, delay, unit) runs the task once after a delay and returns a ScheduledFuture; scheduleAtFixedRate(task, initialDelay, period, unit) fires every period regardless of how long the task takes; scheduleWithFixedDelay(task, initialDelay, delay, unit) waits delay between the end of one run and the start of the next. The thread pool means several scheduled tasks can run concurrently up to the core size. You must call shutdown() (or use try-with-resources in Java 19+ since it is AutoCloseable) to stop the threads; otherwise non-daemon worker threads keep the JVM alive. It replaced the legacy java.util.Timer, which used a single thread and let one failing task kill all the rest.

code

java · 13 lines
java
ScheduledExecutorService exec = Executors.newScheduledThreadPool(2);

// one-shot: run once after 5 seconds
ScheduledFuture<?> once = exec.schedule(
    () -> System.out.println("fired once"), 5, TimeUnit.SECONDS);

// repeating: start now, then every 10 seconds
exec.scheduleAtFixedRate(
    () -> System.out.println("poll"), 0, 10, TimeUnit.SECONDS);

// graceful shutdown later
exec.shutdown();
exec.awaitTermination(30, TimeUnit.SECONDS);

go deeper

for a junior

Knows it runs tasks after a delay or on a repeat, creates one with Executors.newScheduledThreadPool, and remembers to shut it down.

for a middle

Distinguishes the three scheduling methods, knows schedule() returns a ScheduledFuture, and understands non-daemon threads keep the JVM alive.

for a senior

Explains the pool/delay-queue model, why it replaced java.util.Timer, daemon-thread and shutdown strategies, and the periodic-exception cancellation hazard.

for a principal

Reasons about pool sizing, drift vs. dedicated schedulers (Quartz/cron), back-pressure when tasks outrun the rate, and observability/retry policy for failing periodic jobs across a fleet.

## The problem it solves Sometimes you do not want to run code *now* — you want to run it **after a delay** ("send a reminder in 30 seconds") or **repeatedly on a schedule** ("poll the health endpoint every 10 seconds"). A plain thread or a normal `ExecutorService` only runs work as soon as a thread is free; it has no notion of *time*. `ScheduledExecutorService` adds that notion. ## What an Executor is (foundation) An **Executor** is an object you hand tasks to; it decides which thread runs them. A **task** is a unit of work: a `Runnable` (its `run()` returns nothing) or a `Callable<V>` (its `call()` returns a value of type `V`). An `ExecutorService` is an Executor that also lets you track results via a **Future** (a handle to a result that may not exist yet) and lets you shut the pool down. A **thread pool** is a fixed set of reusable worker threads, so you do not pay the cost of creating a new OS thread per task. ## ScheduledExecutorService `ScheduledExecutorService` *extends* `ExecutorService` and adds four time-aware methods. You create one with a factory in `java.util.concurrent.Executors`: - `Executors.newScheduledThreadPool(int corePoolSize)` — a pool with that many worker threads. - `Executors.newSingleThreadScheduledExecutor()` — exactly one worker thread (tasks never overlap). The concrete class behind both is `ScheduledThreadPoolExecutor`, which keeps tasks in a **delay queue** ordered by their next execution time; a worker picks the task whose time has arrived. ## The four scheduling methods 1. `schedule(Runnable command, long delay, TimeUnit unit)` — run **once** after `delay`. Returns a `ScheduledFuture<?>`. 2. `schedule(Callable<V> callable, long delay, TimeUnit unit)` — same, but the task produces a value you can read from the returned `ScheduledFuture<V>`. 3. `scheduleAtFixedRate(Runnable, long initialDelay, long period, TimeUnit)` — run repeatedly; each run *starts* one `period` after the previous run *started* (fixed **rate**). 4. `scheduleWithFixedDelay(Runnable, long initialDelay, long delay, TimeUnit)` — run repeatedly; each run starts `delay` after the previous run *finished* (fixed **delay**/gap). `TimeUnit` is an enum (`SECONDS`, `MILLISECONDS`, …) so the number is unambiguous. ## Lifecycle — you must shut it down The pool's worker threads are **non-daemon** by default, meaning the JVM will not exit while they exist. So you must stop the service: - `shutdown()` — stop accepting new tasks, let already-submitted ones finish. - `shutdownNow()` — also try to interrupt running tasks and return the queued-but-not-started ones. - `awaitTermination(timeout, unit)` — block until the pool is fully stopped. Since Java 19, `ExecutorService` implements `AutoCloseable`, so `try (var ex = Executors.newScheduledThreadPool(2)) { … }` shuts it down automatically. ## Why not java.util.Timer? The old `java.util.Timer`/`TimerTask` used a **single** background thread and was fragile: an unchecked exception in one task killed the timer thread and silently stopped every other scheduled task; it also used absolute system-clock time, so a clock change could misfire tasks. `ScheduledExecutorService` is the modern replacement: pooled threads, relative timing, and per-task `ScheduledFuture` control. ## A note on failures For **periodic** tasks, an uncaught exception does not crash the pool — but it *does* silently cancel **that task's** future runs. So you almost always wrap a periodic task's body in try/catch. (That hazard has its own question.)

  • What happens to the JVM if you never call shutdown() on a ScheduledExecutorService?
    Its worker threads are non-daemon by default, so they keep running and the JVM will not exit on its own — the program hangs after main() returns. Either shut it down, use try-with-resources (Java 19+), or supply a ThreadFactory that creates daemon threads.
  • How is it better than java.util.Timer?
    It uses a thread pool instead of one thread, so a long task does not block others; an exception in one task does not silently kill the whole scheduler thread (only that periodic task's future runs); and it uses relative delays rather than the absolute wall clock, so clock adjustments do not misfire tasks.

Think of it like an alarm clock attached to a small crew of workers: you set alarms ('do X in 30s', 'do Y every 5s'), and when each alarm rings a free worker picks up that job.

saying these in an interview costs you the question

  • Saying it is unrelated to ExecutorService — it directly extends it.
  • Claiming the JVM exits even though the pool is still running (workers are non-daemon by default).
  • Believing a single failing task crashes the whole pool — for periodic tasks only that task's future runs are cancelled.
  • Recommending java.util.Timer as the modern choice.

context

open as a page

What is the difference between scheduleAtFixedRate and scheduleWithFixedDelay, and how does task duration affect each?

level: middleimportance: must knowfreq 70%

basics

~20 s

FixedRate measures the period from the start of one run to the start of the next, so it tries to run every N units. FixedDelay measures the gap from the end of one run to the start of the next. If a task runs longer than the rate period, fixedRate runs do not overlap (they queue back-to-back) while fixedDelay always leaves the full gap.

open as a page

What happens to a periodic task scheduled on a ScheduledExecutorService if its body throws an uncaught exception, and how do you handle it safely?

level: seniorimportance: must knowfreq 66%

basics

~20 s

If a periodic task throws an uncaught exception, that task is silently cancelled — it never runs again — and you usually see no log. The fix is to wrap the task body in try/catch so the exception never escapes, or inspect the returned ScheduledFuture.

open as a page

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

level: middleimportance: should knowfreq 48%

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.

open as a page

How would you size and operate a ScheduledExecutorService for many recurring jobs, and what are the limits of its timing guarantees compared to a dedicated scheduler?

level: principalimportance: should knowfreq 34%

basics

~20 s

Size the pool so concurrently-due tasks aren't starved: a single thread serializes everything, so one slow job delays the rest. ScheduledExecutorService gives relative, best-effort timing with no persistence, no cron expressions, and no clustering — for those you reach for Quartz, a cron service, or a distributed scheduler.

open as a page