What is a ScheduledExecutorService and how do you use it to run a task after a delay or on a repeating schedule?
answer
- extends ExecutorService + time
- schedule / atFixedRate / withFixedDelay
- Executors.newScheduledThreadPool
- TimeUnit on every call
- must shutdown() — replaces java.util.Timer
basics
~10 sIt 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 sScheduledExecutorService 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 linesScheduledExecutorService 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
Knows it runs tasks after a delay or on a repeat, creates one with Executors.newScheduledThreadPool, and remembers to shut it down.
Distinguishes the three scheduling methods, knows schedule() returns a ScheduledFuture, and understands non-daemon threads keep the JVM alive.
Explains the pool/delay-queue model, why it replaced java.util.Timer, daemon-thread and shutdown strategies, and the periodic-exception cancellation hazard.
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.