Resolving Execution Delay and Single‑Thread Issues in SpringBoot @EnableScheduling
This article explains why SpringBoot's default @EnableScheduling creates a single‑threaded scheduler that can cause execution delays, and demonstrates multiple solutions—including configuring the thread pool, using async execution, and applying a distributed lock with Redisson—to achieve reliable and concurrent scheduled tasks.
SpringBoot provides several ways to implement scheduled tasks, primarily using the @EnableScheduling and @Scheduled annotations. The default configuration creates a ThreadPoolTaskScheduler with a core pool size of 1, which leads to execution delays and single‑threaded processing when tasks take longer than the schedule interval.
The article first shows the basic project setup with Maven dependencies and a simple scheduled service that logs the thread ID every five seconds.
<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</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>The initial implementation uses a single method annotated with @Scheduled(cron = "0/5 * * * * ?") . When the method sleeps for 10 seconds, the log shows a 15‑second interval instead of the expected 5 seconds, confirming the single‑thread bottleneck.
@Scheduled(cron = "0/5 * * * * ?")
public void testSchedule() {
try {
Thread.sleep(10000);
log.info("当前执行任务的线程号ID===>{}", Thread.currentThread().getId());
} catch (Exception e) {
e.printStackTrace();
}
}Root cause analysis reveals that @EnableScheduling auto‑configures TaskSchedulingAutoConfiguration , which creates a ThreadPoolTaskScheduler with poolSize = 1 . The default ScheduledExecutorService also uses unbounded queue sizes, which is unsafe for production.
Four solution approaches are presented:
1. Configure the built‑in scheduler
spring:
task:
scheduling:
thread-name-prefix: nzc-schedule-
pool:
size: 10After setting size: 10 , logs show multiple threads (nzc‑schedule‑1, nzc‑schedule‑2, …) handling the tasks, eliminating the delay.
2. Delegate execution to a custom thread pool
@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;
}
}The scheduled method now submits its work to this executor via CompletableFuture.runAsync , resulting in distinct threads for each execution.
3. Make the scheduled method asynchronous
@EnableAsync
@EnableScheduling
@Component
public class ScheduleService {
@Autowired
TaskExecutor taskExecutor;
@Async(value = "taskExecutor")
@Scheduled(cron = "0/5 * * * * ?")
public void testSchedule() {
try {
Thread.sleep(10000);
log.info("当前执行任务的线程号ID===>{}", Thread.currentThread().getId());
} catch (Exception e) {
e.printStackTrace();
}
}
}Adding @Async with the custom executor also removes the delay.
4. Distributed lock for multi‑instance scenarios
In a distributed environment, multiple instances could run the same scheduled job, causing duplicate processing. The article suggests using Redisson to acquire a lock before executing the task.
@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);
}
} @Component
@EnableAsync
@EnableScheduling
public class ScheduleService {
@Autowired
TaskExecutor taskExecutor;
@Autowired
RedissonClient redissonClient;
private final String SCHEDULE_LOCK = "schedule:lock";
@Async(value = "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("当前执行任务的线程号ID===>{}", Thread.currentThread().getId());
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}The lock ensures that only one instance runs the job at a time, preventing data duplication. The article also discusses potential failure handling and compensation strategies.
In conclusion, the default SpringBoot scheduler is suitable only for simple monolithic applications; for production or distributed systems, configuring a proper thread pool, using async execution, or applying a distributed lock are essential to achieve reliable scheduled tasks.
Java Architect Essentials
Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.
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.