Spring Boot Handbook

    Asynchronous Task Scheduling in Spring Boot

    Introduction#

    It should be noted that there are various performance degradation areas associated with a long-running or time-sequenced task during synchronization. Their asynchronous execution capability places them among all other tasks in the application context and, finally, leads to their efficient execution without blocking the application thread.

    Spring Boot has built-in asynchronous task execution support with:#

    • @Async Annotation: Runs the method in a separate thread.
    • TaskScheduler and @Scheduled Annotation: Automatically execute periodic tasks with fixed intervals.
    • Executor Configuration: Controls the configuration of thread pools for the concurrent running of tasks.

    All these techniques help to achieve effective performance optimization while keeping the application running smoothly.

    Default @Scheduled#

    @Scheduled tasks in Spring Boot run single-threaded by default through a single-thread executor. A delay occurs on the next execution when the task goes beyond the scheduled interval.

    Use for concurrent execution:

    1. @EnableAsync along with @EnableScheduling.
    2. @Asyncis a method annotated with @Scheduled to run the task on a different thread.

    Scheduling tasks on a particular custom thread pool executor will ensure that the scheduled tasks run without blocking each other, resulting in a good performance boost for the application itself.

    Async Task Scheduling#

    To run long-residing jobs independently so that they do not block other jobs, use an @Async with @Scheduledone.

    Steps:#

    Enable async execution: Put @EnableAsync and @EnableScheduling.

    import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; import java.time.Instant; @SpringBootApplication @Slf4j @EnableScheduling @EnableAsync public class LearnAsyncSchedulingApplication implements CommandLineRunner { @Autowired private TaskScheduler taskScheduler; public static void main(String[] args) { SpringApplication.run(LearnAsyncSchedulingApplication.class, args); } @Override public void run(String... args) throws Exception { taskScheduler.schedule(() -> { log.info("Running after 2 sec"); }, Instant.ofEpochSecond(2)); } }

    Mark methods with@Async: Each scheduled task runs in a separate thread.

    @Scheduled(fixedRate = 200) // NOT concurrent @Async() void logMe() { log.info("Scheduler1 started... {}", Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } }

    Benefits:#

    • task1() and task2() are independent.
    • No long execution will affect blocking.
    • Great application performance.

    Custom Executor#

    By default, Spring Boot creates unlimited threads for its @Async; memory leaks, therefore, could result.

    Solution: Use a Custom Executor#

    • Define an Executor Bean with a fixed thread pool.
    import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.RejectedExecutionHandler; import java.util.concurrent.ThreadPoolExecutor; @Configuration public class ThreadConfig { @Bean public ThreadPoolTaskExecutor jobExecutor() { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); taskExecutor.setCorePoolSize(4); taskExecutor.setMaxPoolSize(10); taskExecutor.setQueueCapacity(6); taskExecutor.setThreadNamePrefix("JobExec-"); taskExecutor.setRejectedExecutionHandler(new RejectedExecutionHandler() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { } }); taskExecutor.initialize(); return taskExecutor; } }
    • Specify the Executor in @Async("customTaskExecutor").
    import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Component @Slf4j public class MyScheduler { @Scheduled(fixedRate = 200) // NOT concurrent @Async("jobExecutor") void logMe() { log.info("Scheduler1 started... {}", Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }

    Output:#

    1. @Async() (Default Executor)
      • Uses Spring Boot's default task executor (SimpleAsyncTaskExecutor).
      • Creates a new thread per task, leading to unlimited thread creation.
      • Output will show dynamically generated thread names like:
    Output will show dynamically generated thread 
    1. @Async("jobExecutor") (Custom Executor)
      • Uses a customly defined thread pool (e.g., ThreadPoolTaskExecutor).
      • Thread count is limited based on pool size settings.
      • Output will show controlled thread names like:
    Output will show controlled thread

    Benefits:#

    • Prevents excessive thread creation.
    • Optimizes performance with controlled concurrency.
    • Avoids memory leaks by limiting threads.

    Conclusion#

    This article introduces the Spring Boot asynchronous task-scheduling framework through @Scheduled and @Async to run tasks in parallel. It comments on the limitations of the default executor and the possibility of running out of control while creating threads. To enhance performance and avert memory leaks, it proposes that a custom thread-pool executor be opted for so that concurrency can be controlled.

    Last updated on Apr 09, 2025