Spring Boot Scheduling: Serial vs Parallel Execution, Custom Thread Pools, and Exception Handling

This article explains how to configure Spring Boot scheduled tasks, compare serial and parallel execution using @Async, create custom and global thread pools, and handle asynchronous exceptions, providing complete code examples and practical observations.

Top Architect
Top Architect
Top Architect
Spring Boot Scheduling: Serial vs Parallel Execution, Custom Thread Pools, and Exception Handling

The project uses JDK 1.8+ and Spring Boot with @EnableScheduling in the main class. A ScheduledTask component defines two simple scheduled methods that print the thread name and timestamp.

@Component
public class ScheduledTask {
    @Scheduled(cron = "0/1 * * * * ?")
    public void scheduledTask1() throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "---scheduledTask1 " + System.currentTimeMillis());
    }

    @Scheduled(cron = "0/1 * * * * ?")
    public void scheduledTask2() {
        System.out.println(Thread.currentThread().getName() + "---scheduledTask2 " + System.currentTimeMillis());
    }
}

1. Serial execution of multiple tasks

When both tasks run at the same schedule, the console shows no deterministic order; the tasks execute in a random sequence, indicating no built‑in priority.

Adding a blocking loop to scheduledTask1 makes the second task wait, demonstrating that serial scheduled tasks share the same thread pool and a blocked task blocks the others.

@Scheduled(cron = "0/1 * * * * ?")
public void scheduledTask1() throws InterruptedException {
    System.out.println(Thread.currentThread().getName() + "---scheduledTask1 " + System.currentTimeMillis());
    while (true) {
        Thread.sleep(5000);
    }
}

2. Parallel execution

To run tasks concurrently, add @EnableAsync to the application and annotate the methods with @Async. Each execution creates a new thread using the default SimpleAsyncTaskExecutor, which does not reuse threads.

@Component
public class ScheduledTask {
    @Scheduled(cron = "0/1 * * * * ?")
    @Async
    public void scheduledTask1() throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "---scheduledTask1 " + System.currentTimeMillis());
    }

    @Scheduled(cron = "0/1 * * * * ?")
    @Async
    public void scheduledTask2() {
        System.out.println(Thread.currentThread().getName() + "---scheduledTask2 " + System.currentTimeMillis());
    }
}

Because the default executor creates a new thread for every call, it is recommended to define a custom thread pool.

3. Custom thread pool (local)

@Component
public class AsyncTaskExecutorConfig {
    @Bean("asyncTaskExecutor")
    public AsyncTaskExecutor asyncTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setThreadNamePrefix("defineAsyncTask-");
        executor.setCorePoolSize(3);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(60);
        executor.initialize();
        return executor;
    }
}

Tasks can then specify this executor:

@Scheduled(cron = "0/1 * * * * ?")
@Async("asyncTaskExecutor")
public void scheduledTask1() throws InterruptedException { ... }

4. Global thread pool

@Configuration
public class AsyncGlobalConfig extends AsyncConfigurerSupport {
    private static final String THREAD_PREFIX = "defineGlobalAsync-";
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setThreadNamePrefix(THREAD_PREFIX);
        executor.setCorePoolSize(3);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setKeepAliveSeconds(60);
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(60);
        executor.initialize();
        return executor;
    }
}

When no specific executor is referenced, this global pool is used.

5. Exception handling for async tasks

For submit() tasks, Future.get() propagates exceptions. For execute() (void return) tasks, implement AsyncUncaughtExceptionHandler:

static class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
    @Override
    public void handleUncaughtException(Throwable e, Method method, Object... args) {
        // custom handling logic
    }
}

Override getAsyncUncaughtExceptionHandler() in the global config to return this handler.

Overall, the article demonstrates how to set up scheduled tasks in Spring Boot, observe serial vs parallel behavior, configure both local and global thread pools, and properly handle asynchronous exceptions.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaThreadPoolSchedulingSpringBootAsync
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.