Master Asynchronous Programming in Spring Boot: From Basics to Custom Thread Pools
This article explains why asynchronous execution is needed in Spring Boot, demonstrates how to enable @Async, shows step‑by‑step code for configuration, method annotation, and controller usage, and details how to create and manage custom thread pools for optimal performance and fault tolerance.
Why Use Asynchronous Programming in Spring Boot?
In typical Spring Boot development calls are synchronous, but scenarios like user registration with bonus points or order confirmation with push notifications benefit from async execution to improve performance and fault tolerance.
Fault tolerance: secondary tasks (e.g., awarding points) should not cause the primary operation (registration) to fail.
Performance: parallel execution reduces total response time.
Enabling Async Support in Spring Boot
Spring provides the
@Asyncannotation for async methods and the
@EnableAsyncannotation to activate the feature.
Step 1 – Create a Configuration Class
<code>@Configuration
@EnableAsync
public class AsyncConfiguration {
}
</code>Step 2 – Annotate Business Methods
<code>@Component
@Slf4j
public class AsyncTask {
@SneakyThrows
@Async
public void doTask1() {
long t1 = System.currentTimeMillis();
Thread.sleep(2000);
long t2 = System.currentTimeMillis();
log.info("task1 cost {} ms", t2 - t1);
}
@SneakyThrows
@Async
public void doTask2() {
long t1 = System.currentTimeMillis();
Thread.sleep(3000);
long t2 = System.currentTimeMillis();
log.info("task2 cost {} ms", t2 - t1);
}
}
</code>Step 3 – Call Async Methods from a Controller
<code>@RestController
@RequestMapping("/async")
@Slf4j
public class AsyncController {
@Autowired
private AsyncTask asyncTask;
@RequestMapping("/task")
public void task() throws InterruptedException {
long t1 = System.currentTimeMillis();
asyncTask.doTask1();
asyncTask.doTask2();
Thread.sleep(1000);
long t2 = System.currentTimeMillis();
log.info("main cost {} ms", t2 - t1);
}
}
</code>Visiting
http://localhost:8080/async/taskshows that the main thread finishes in about 1 s while the async tasks run independently, confirming reduced response time.
Why Provide a Custom Thread Pool for @Async?
The default
SimpleAsyncTaskExecutorcreates a new thread for each task and does not reuse threads, which can quickly exhaust memory and cause
OutOfMemoryError.
Spring’s Built‑in Executors
Spring offers several executors: SimpleAsyncTaskExecutor : creates a new thread per task, no reuse. SyncTaskExecutor : executes tasks synchronously. ConcurrentTaskExecutor : adapter, rarely needed. ThreadPoolTaskScheduler : supports cron expressions. ThreadPoolTaskExecutor : most common, wraps java.util.concurrent.ThreadPoolExecutor .
Defining a Custom Thread Pool
<code>@Configuration
@EnableAsync
public class SyncConfiguration {
@Bean(name = "asyncPoolTaskExecutor")
public ThreadPoolTaskExecutor executor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(10);
taskExecutor.setMaxPoolSize(100);
taskExecutor.setQueueCapacity(50);
taskExecutor.setKeepAliveSeconds(200);
taskExecutor.setThreadNamePrefix("async-");
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
taskExecutor.initialize();
return taskExecutor;
}
}
</code>After registering this bean, @Async will use the custom pool.
Handling Multiple Thread Pools
High‑concurrency endpoints can be isolated by assigning each a dedicated pool. Specify the pool name in the annotation, e.g.
@Async("asyncPoolTaskExecutor")or
@Async("otherTaskExecutor"). A default pool can be supplied by implementing
AsyncConfigurerand overriding
getAsyncExecutor().
Example AsyncConfigurer
<code>@Configuration
@EnableAsync
@Slf4j
public class AsyncConfiguration implements AsyncConfigurer {
@Bean(name = "asyncPoolTaskExecutor")
public ThreadPoolTaskExecutor executor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(2);
taskExecutor.setMaxPoolSize(10);
taskExecutor.setQueueCapacity(50);
taskExecutor.setKeepAliveSeconds(200);
taskExecutor.setThreadNamePrefix("async-");
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
taskExecutor.initialize();
return taskExecutor;
}
@Override
public Executor getAsyncExecutor() {
return executor();
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) ->
log.error("Async task error in method: {}", method.getName(), ex);
}
}
</code>Now
doTask1()runs on the default
asyncPoolTaskExecutor, while
doTask2()can be directed to
otherTaskExecutorfor flexible resource allocation.
Conclusion
Async methods annotated with
@Asyncare a powerful tool in Spring Boot development. Understanding how to enable them, provide a proper thread pool, and manage multiple pools is essential for building performant and resilient services.
macrozheng
Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.
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.