Why SpringBoot @EnableScheduling Is Slow and How to Fix It
This article explains why SpringBoot's default @EnableScheduling runs tasks sequentially with noticeable delays, analyzes the underlying single‑threaded scheduler, and provides multiple solutions—including thread‑pool configuration, asynchronous execution, custom executors, and distributed locks—to achieve reliable and scalable scheduled jobs.
1. Basic Environment Setup
Include the essential Maven dependencies:
<parent>
<artifactId>spring-boot-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.7.2</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>and create a main class annotated with @SpringBootApplication to start the application.
2. Problem: Execution Delay and Single‑Threaded Execution
Using @Scheduled(cron = "0/5 * * * * ?") should trigger every five seconds. The expected timestamps are 00:21:10, 00:21:15, 00:21:20, etc., but the actual logs show the task runs at exactly those times only when the job finishes quickly. When the task sleeps for ten seconds, the next execution is delayed by ten seconds, and only one thread (ID 64) processes all runs, causing a backlog.
3. Why the Problem Occurs
The root cause is SpringBoot's TaskSchedulingAutoConfiguration, which creates a ThreadPoolTaskScheduler with a core pool size of 1. Consequently, all scheduled jobs share a single thread, leading to execution delay and blocking when a job takes longer than its interval. The default thread‑pool also uses Integer.MAX_VALUE for max threads and queue size, which is unsafe for production.
4. Solutions
4.1 Modify Configuration File
Adjust the scheduler properties in application.yml or application.properties:
spring:
task:
scheduling:
thread-name-prefix: nzc-schedule-
pool:
size: 10After the change, logs show multiple threads (e.g., nzc‑schedule‑1, nzc‑schedule‑2) handling the job, eliminating the single‑thread bottleneck.
4.2 Asynchronous Execution with a Custom Thread Pool
Define a configuration class that provides a TaskExecutor bean:
@Configuration
public class MyThreadPoolConfig {
@Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(200);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("nzc-create-scheduling-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
return executor;
}
}Inject this executor into the scheduled service and run the job asynchronously with CompletableFuture.runAsync(..., taskExecutor). The logs now display different thread IDs for each execution, confirming parallel processing.
4.3 Async Scheduling with @Async
Add @EnableAsync to the configuration and annotate the scheduled method with @Async("taskExecutor"):
@EnableAsync
@EnableScheduling
@Component
public class ScheduleService {
@Autowired
TaskExecutor taskExecutor;
@Async("taskExecutor")
@Scheduled(cron = "0/5 * * * * ?")
public void testSchedule() {
try {
Thread.sleep(10000);
log.info("Current thread ID => {}", Thread.currentThread().getId());
} catch (Exception e) {
e.printStackTrace();
}
}
}Each execution runs in its own thread (te‑scheduling‑1, te‑scheduling‑2, …), eliminating delay even when the job lasts longer than the interval.
4.4 Distributed Lock for Multi‑Node Scenarios
When multiple instances run the same scheduled job, use Redisson to acquire a distributed lock before execution:
@Configuration
public class MyRedissonConfig {
@Bean(destroyMethod = "shutdown")
public RedissonClient redissonClient() throws IOException {
Config config = new Config();
config.useSingleServer().setAddress("redis://xxxxx:6379").setPassword("000415");
return Redisson.create(config);
}
}In the scheduled service:
@EnableAsync
@EnableScheduling
@Component
public class ScheduleService {
@Autowired
TaskExecutor taskExecutor;
@Autowired
RedissonClient redissonClient;
private final String SCHEDULE_LOCK = "schedule:lock";
@Async("taskExecutor")
@Scheduled(cron = "0/5 * * * * ?")
public void testSchedule() {
RLock lock = redissonClient.getLock(SCHEDULE_LOCK);
try {
lock.lock(10, TimeUnit.SECONDS);
Thread.sleep(10000);
log.info("Current thread ID => {}", Thread.currentThread().getId());
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}The lock ensures only one node executes the job at a time, preventing duplicate processing in a distributed environment.
5. Summary
SpringBoot's default scheduling uses a single‑threaded pool, which leads to execution delays and blocking for long‑running tasks. By configuring a larger thread pool, leveraging asynchronous execution, or applying a distributed lock with Redisson, developers can achieve reliable, scalable scheduled jobs both in single‑node and multi‑node deployments.
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.
Java Backend Technology
Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!
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.
