Spring Boot Scheduling: Timer vs ScheduledExecutorService vs @Scheduled
This article compares three single‑node scheduling approaches in Spring Boot—Java's legacy Timer, the modern ScheduledExecutorService, and Spring's @Scheduled annotation—showing their implementations, execution characteristics, thread handling, error behavior, and when to prefer each solution.
Timer (legacy)
Timer, introduced in Java 1.3, schedules tasks with a single thread. The example creates two TimerTask instances, one of which sleeps for 6 seconds. Running the program prints:
开始执行了。。。Mon Dec 16 10:32:19 CST 2024
task1 run:Mon Dec 16 10:32:19 CST 2024
task2 run:Mon Dec 16 10:32:25 CST 2024The output shows that task2 starts only after task1 finishes, confirming the single‑threaded behavior. If task1 throws an exception, the timer thread terminates and the second task never runs, which is why the Alibaba Java Development Manual explicitly discourages using Timer for scheduling.
ScheduledExecutorService (modern)
Introduced in Java 1.5 as part of java.util.concurrent, ScheduledExecutorService uses a thread pool, allowing concurrent execution of multiple tasks. Example:
public static void main(String[] args) {
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(3);
// one‑time delayed task
scheduler.schedule(() -> System.out.println("task1 run: " + new Date()), 5, TimeUnit.SECONDS);
// fixed‑rate task
scheduler.scheduleAtFixedRate(() -> System.out.println("task2 run: " + new Date()), 1, 2, TimeUnit.SECONDS);
// fixed‑delay task
scheduler.scheduleWithFixedDelay(() -> System.out.println("task3 run: " + new Date()), 1, 2, TimeUnit.SECONDS);
}Execution logs show interleaved output from different threads (e.g., pool-1-thread-1, pool-1-thread-2), demonstrating true multithreading and isolation: a failure in one task does not affect the others.
The interface defines three core methods: schedule(Runnable command, long delay, TimeUnit unit) – one‑time delayed execution.
scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)– repeated execution at a fixed rate.
scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)– repeated execution with a fixed delay after each run.
The concrete implementation class is ScheduledThreadPoolExecutor, which extends ThreadPoolExecutor and inherits full thread‑pool capabilities.
Task cancellation can be performed via the returned Future:
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
ScheduledFuture<?> future = scheduler.schedule(() -> System.out.println("Task executed"), 5, TimeUnit.SECONDS);
future.cancel(false);Spring @Scheduled (Spring Scheduler)
Spring Scheduler wraps an underlying executor and provides declarative scheduling via annotations. A typical component:
@Component
@Slf4j
public class ScheduledTask {
@Scheduled(fixedRate = 5000)
public void taskWithFixedRate() {
log.info("Fixed Rate Task: " + new Date());
}
@Scheduled(fixedDelay = 5000, initialDelay = 3000)
public void taskWithFixedDelay() {
log.info("Fixed Delay Task: " + new Date());
}
@Scheduled(cron = "0/10 * * * * ?")
public void taskWithCron() {
log.info("Cron Task: " + new Date());
}
}The application class must enable scheduling with @EnableScheduling:
@SpringBootApplication
@EnableScheduling
public class BaseDemoApplication {
public static void main(String[] args) {
SpringApplication.run(BaseDemoApplication.class, args);
}
}Log output shows that all three tasks share the same thread pool and execute sequentially. When a task blocks (e.g., TimeUnit.SECONDS.sleep(8)), subsequent tasks wait, illustrating the default single‑threaded nature of Spring’s scheduler.
Two separate task classes using the same cron expression demonstrate that, without additional configuration, Spring runs them on the same thread, causing serial execution:
// Task 1
@Component
public class ScheduledTask {
@SneakyThrows
@Scheduled(cron = "0/10 * * * * ?")
public void task1() {
log.info("task1: " + new Date());
TimeUnit.SECONDS.sleep(8);
}
}
// Task 2
@Component
public class ScheduledTask2 {
@Scheduled(cron = "0/10 * * * * ?")
public void task2() {
log.info("task2: " + new Date());
}
} [2024-12-16 17:00:30] task1: 2024-12-16 17:00:30
[2024-12-16 17:00:38] task2: 2024-12-16 17:00:38Task 2 starts only after Task 1 finishes, confirming serial execution.
Combining @Async with @Scheduled for parallel execution
Define a custom thread pool bean:
@Configuration
public class AsyncConfig {
@Bean(name = "asyncExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(200);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("asyncExecutor-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
}Enable async processing in the main class:
@SpringBootApplication
@EnableAsync
@EnableScheduling
public class AsyncScheduledTaskApplication {
public static void main(String[] args) {
SpringApplication.run(AsyncScheduledTaskApplication.class, args);
}
}Apply @Async("asyncExecutor") to @Scheduled methods:
@Component
public class ScheduledTask {
@SneakyThrows
@Scheduled(cron = "0/10 * * * * ?")
@Async("asyncExecutor")
public void task1() {
log.info("task1: " + new Date());
TimeUnit.SECONDS.sleep(8);
}
}
@Component
public class ScheduledTask2 {
@Async("asyncExecutor")
@Scheduled(cron = "0/10 * * * * ?")
public void task2() {
log.info("task2: " + new Date());
}
}Log output shows different threads (e.g., asyncExecutor-3, asyncExecutor-2) executing the two tasks concurrently:
[asyncExecutor-3] task1: 2024-12-16 17:17:40
[asyncExecutor-2] task2: 2024-12-16 17:17:40
[asyncExecutor-7] task2: 2024-12-16 17:17:50
[asyncExecutor-4] task1: 2024-12-16 17:17:50Feature comparison
Introduced : Timer (Java 1.3, legacy), ScheduledExecutorService (Java 1.5), @Scheduled (Spring Framework).
Configuration : Timer and ScheduledExecutorService are programmatic; @Scheduled is annotation‑based.
Thread‑pool support : Timer – none (single thread); ScheduledExecutorService – user‑managed pool; @Scheduled – built‑in Spring‑managed pool (single thread by default, can be customized).
Cron expression : Not built‑in for Timer or ScheduledExecutorService (requires extra handling); native support in @Scheduled.
Task isolation : ScheduledExecutorService provides true multithreaded isolation; @Scheduled inherits the same semantics when a pool >1 is configured; Timer runs tasks sequentially, so a failure stops subsequent tasks.
Complex scheduling : ScheduledExecutorService is highly flexible for advanced scenarios; @Scheduled is best for lightweight, Spring‑centric jobs.
Distributed support : ScheduledExecutorService can be combined with external tools; @Scheduled requires extensions such as Quartz for distributed scheduling.
Summary
Avoid Timer because its single‑threaded execution and uncaught‑exception behavior can cause task loss. Prefer ScheduledExecutorService for robust multithreaded scheduling, and use @Scheduled for quick integration within Spring Boot projects. When using @Scheduled, configure an appropriate thread‑pool size (or combine with @Async) to prevent a single blocking task from halting all scheduled jobs.
Relevant repository references:
GitHub: https://github.com/plasticene/plasticene-boot-starter-parent
Gitee: https://gitee.com/plasticene3/plasticene-boot-starter-parent
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Shepherd Advanced Notes
Dedicated to sharing advanced Java technical insights, daily work snippets, and the power of persistent effort.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
