Mastering SpringBoot @Async: Thread Pools, Custom Executors, and Virtual Threads
This guide explains how SpringBoot's @Async annotation works, why to use it, how to configure default and custom thread pools, handle exceptions and return values, and leverage virtual threads for higher throughput in Java backend applications.
1. Introduction
In a SpringBoot 3.2.5 environment, the @Async annotation marks methods for asynchronous execution, allowing them to run in separate threads without blocking the caller, which improves system concurrency and response speed.
Typical reasons to use @Async are:
Improve system performance : time‑consuming operations such as heavy database queries, complex calculations, or remote service calls run in background threads, freeing the main thread.
Enhance user experience : web pages do not have to wait for long‑running tasks before responding to user actions.
Simplify code : the annotation abstracts away low‑level thread handling, letting developers focus on business logic.
2. Application Details
2.1 Default Thread Pool
By default SpringBoot allows up to 8 concurrent async tasks; additional tasks wait in a queue whose capacity is Integer.MAX_VALUE.
@Resource
private TaskService taskServie;
public void task() throws Throwable {
this.taskServie.task1();
this.taskServie.task2();
this.taskServie.task3();
this.taskServie.task4();
this.taskServie.task5();
this.taskServie.task6();
this.taskServie.task7();
this.taskServie.task8();
this.taskServie.task9();
}
@Async
public void task1() throws Exception {
System.err.printf("当前执行的线程: %s%n", Thread.currentThread().getName());
TimeUnit.SECONDS.sleep(100);
}
// other 8 async tasks are similar to task1Running the above prints thread names for tasks 1‑8; the ninth task enters the waiting queue.
当前执行的线程: task-1
当前执行的线程: task-2
当前执行的线程: task-3
当前执行的线程: task-4
当前执行的线程: task-5
当前执行的线程: task-6
当前执行的线程: task-7
当前执行的线程: task-8The ninth task is queued.
Configuring Thread Pool Size
spring:
task:
execution:
pool:
core-size: 10
max-size: 20
queue-capacity: 1002.2 Custom Thread Pool via AsyncConfigurer
@Component
public class PackAsyncConfigurer implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.afterPropertiesSet();
return executor;
}
}When a custom executor is defined programmatically, the properties in application.yml are ignored.
2.3 Specifying Executor for a Particular Async Task
@Bean
ThreadPoolTaskExecutor taskExecutor1() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// configure executor
return executor;
}
@Async("taskExecutor1")
public void task4() throws Exception {
// task implementation
}You can create multiple executors and assign them to different async methods, but be mindful of each pool’s size.
2.4 Async Exception Handling
By default, uncaught exceptions from async methods are logged by SimpleAsyncUncaughtExceptionHandler . You can provide a custom handler via AsyncConfigurer:
@Component
public class PackAsyncConfigurer implements AsyncConfigurer {
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new AsyncUncaughtExceptionHandler() {
@Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
System.err.printf("异步任务: %s发生异常, 错误消息: %s%n",
method.getDeclaringClass() + "#" + method.getName(), ex.getMessage());
}
};
}
}2.5 Async Return Values
Async methods usually return void, but you can return CompletableFuture<String> (or ListenableFuture, Future) to obtain results.
@Async
public CompletableFuture<String> runTask() throws Exception {
System.err.printf("当前执行的线程: %s%n", Thread.currentThread().getName());
TimeUnit.SECONDS.sleep(1);
return CompletableFuture.completedFuture("数据获取成功");
}
public void task() throws Throwable {
System.out.printf("%s start...%n", Thread.currentThread().getName());
CompletableFuture<String> task = this.taskServie.runTask();
String ret = task.join();
System.out.printf("获取结果: %s%n", ret);
System.out.printf("%s end...%n", Thread.currentThread().getName());
}Alternatively, handle the result asynchronously:
this.taskServie.runTask().whenComplete((ret, ex) -> {
if (ex != null) {
System.err.printf("发生错误: %s%n", ex.getMessage());
return;
}
System.out.printf("获取结果: %s%n", ret);
});2.6 Virtual Threads (JDK 21)
SpringBoot 3.2.x supports executing async tasks on virtual threads. Enable it with:
spring:
threads:
virtual:
enabled: trueThe regular thread‑pool configuration remains unchanged, but when virtual threads are used the core/max pool settings become ineffective.
@Async
public void task1() throws Exception {
System.err.printf("当前执行的线程: %s, 是否虚拟线程: %b%n",
Thread.currentThread().getName(), Thread.currentThread().isVirtual());
}Running the method prints something like:
当前执行的线程: vm-pack-task-1, 是否虚拟线程: trueThus the task runs on a virtual thread.
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.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
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.
