In application development, the need to run tasks not immediately, but at some point in the future, is a common requirement. Whether it's sending a reminder email after 24 hours, caching data for a specific duration, or implementing a retry mechanism with a backoff delay, scheduling delayed tasks is a fundamental skill.
While the classic Thread.sleep() combined with a Runnable can achieve a basic delay, it's inefficient and doesn't scale. Java provides a robust, thread-safe solution for this exact purpose: the ScheduledExecutorService.
What is the ScheduledExecutorService?
The ScheduledExecutorService is an interface in the java.util.concurrent package that extends ExecutorService. It allows you to schedule commands to run after a given delay, or to execute periodically at a fixed rate or fixed interval.
The key advantage over manual thread management is that it uses a pool of threads behind the scenes, efficiently managing the execution of multiple scheduled tasks without the overhead of creating a new thread for each one.
Creating a ScheduledExecutorService
You obtain an instance using the Executors factory class. The most common method for delayed tasks is newSingleThreadScheduledExecutor(), which creates a single-threaded executor, perfect for scenarios where task order is important.
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
public class SchedulerExample {
public static void main(String[] args) {
// Create a scheduled executor with a single thread
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
// ... (Schedule tasks here)
// Don't forget to shut down the scheduler when done!
// scheduler.shutdown();
}
}
Scheduling a One-Time Delayed Task
The schedule method is your go-to for running a task once, after a specified delay.
Method Signature:
ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit)
Parameters:
command: TheRunnableorCallabletask to execute.delay: The time from now to delay execution.unit: The time unit of thedelayparameter (e.g.,TimeUnit.SECONDS,TimeUnit.MINUTES,TimeUnit.MILLISECONDS).
Example: A Simple Reminder
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class DelayedTaskDemo {
public static void main(String[] args) {
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
System.out.println("Task submitted at: " + java.time.LocalTime.now());
// Schedule a task to run after a 5-second delay
scheduler.schedule(() -> {
System.out.println("Reminder: Your session will expire soon! " + java.time.LocalTime.now());
}, 5, TimeUnit.SECONDS);
// Shutdown after the task runs (this is a simple demo; real apps manage lifecycle differently)
scheduler.schedule(() -> scheduler.shutdown(), 6, TimeUnit.SECONDS);
}
}
Expected Output:
Task submitted at: 14:30:25.123 Reminder: Your session will expire soon! 14:30:30.456
Scheduling a Task with a Return Value
If your delayed task needs to compute a result, you can use a Callable with the schedule method.
import java.util.concurrent.Callable;
import java.util.concurrent.ScheduledFuture;
public class CallableTaskDemo {
public static void main(String[] args) throws Exception {
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
// Schedule a Callable that returns a result
ScheduledFuture<String> futureResult = scheduler.schedule(new Callable<String>() {
@Override
public String call() throws Exception {
return "The result of a 3-second calculation!";
}
}, 3, TimeUnit.SECONDS);
// .get() blocks until the result is available
String result = futureResult.get();
System.out.println("Result received: " + result);
scheduler.shutdown();
}
}
Important: Shutting Down the Scheduler
An active ScheduledExecutorService will prevent the JVM from shutting down, as its pool threads are non-daemon. You must always manage its lifecycle.
scheduler.shutdown(): Initiates an orderly shutdown. Previously submitted tasks are executed, but no new tasks will be accepted.scheduler.shutdownNow(): Attempts to stop all actively executing tasks and halts the processing of waiting tasks.
A common pattern is to use shutdown() with awaitTermination to give running tasks a chance to finish.
scheduler.shutdown();
try {
// Wait a while for existing tasks to terminate
if (!scheduler.awaitTermination(60, TimeUnit.SECONDS)) {
scheduler.shutdownNow(); // Cancel currently executing tasks
// Wait again for tasks to respond to being cancelled
if (!scheduler.awaitTermination(60, TimeUnit.SECONDS)) {
System.err.println("Scheduler did not terminate");
}
}
} catch (InterruptedException ie) {
scheduler.shutdownNow();
// Preserve interrupt status
Thread.currentThread().interrupt();
}
Conclusion
The ScheduledExecutorService is a powerful and essential tool in the Java developer's concurrency toolkit. It provides a clean, efficient, and safe way to manage delayed and periodic tasks, moving beyond the limitations of manual thread sleeping. By mastering its simple API—primarily the schedule method—you can easily build features that rely on timing, such as notifications, cleanup jobs, and sophisticated retry logic, making your applications more dynamic and robust.
Further Reading: For more complex scheduling needs, explore the scheduleAtFixedRate and scheduleWithFixedDelay methods for periodic task execution.