Using ThreadPoolTaskExecutor and @Async for Asynchronous Processing in Spring Boot
This article explains how to configure a Spring Boot ThreadPoolTaskExecutor, enable asynchronous execution with @Async, create a custom visible executor to log pool statistics, and demonstrates the complete workflow with code examples and runtime logs for improving database insert performance.
In a recent project the author needed to speed up batch inserts into two tables and decided to use a thread pool; the standard ThreadPoolExecutor and Spring Boot’s ThreadPoolTaskExecutor are introduced as the solution.
The first step is to create a configuration class annotated with @Configuration and @EnableAsync , and define a bean named asyncServiceExecutor that reads core pool size, max pool size, queue capacity and thread‑name prefix from application.properties . The bean returns a configured ThreadPoolTaskExecutor and sets a CallerRunsPolicy rejection handler.
@Configuration
@EnableAsync
public class ExecutorConfig {
@Value("${async.executor.thread.core_pool_size}") private int corePoolSize;
@Value("${async.executor.thread.max_pool_size}") private int maxPoolSize;
@Value("${async.executor.thread.queue_capacity}") private int queueCapacity;
@Value("${async.executor.thread.name.prefix}") private String namePrefix;
@Bean(name = "asyncServiceExecutor")
public Executor asyncServiceExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(corePoolSize);
executor.setMaxPoolSize(maxPoolSize);
executor.setQueueCapacity(queueCapacity);
executor.setThreadNamePrefix(namePrefix);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}The application.properties file contains the concrete values, for example:
# Async thread configuration
async.executor.thread.core_pool_size=5
async.executor.thread.max_pool_size=5
async.executor.thread.queue_capacity=99999
async.executor.thread.name.prefix=async-service-A simple service interface AsyncService with a single method executeAsync() is defined, and its implementation is annotated with @Service and @Async("asyncServiceExecutor") . The method logs start and end messages and prints placeholder work such as "异步线程要做的事情".
public interface AsyncService {
void executeAsync();
}
@Service
public class AsyncServiceImpl implements AsyncService {
@Async("asyncServiceExecutor")
public void executeAsync() {
logger.info("start executeAsync");
System.out.println("异步线程要做的事情");
System.out.println("可以在这里执行批量插入等耗时的事情");
logger.info("end executeAsync");
}
}The service is injected into a controller and exposed via an endpoint /async . When the endpoint is called, the controller simply calls asyncService.executeAsync() , allowing the request to return immediately while the heavy work runs in the background.
@Autowired
private AsyncService asyncService;
@GetMapping("/async")
public void async() {
asyncService.executeAsync();
}Runtime logs show multiple threads named async-service‑X executing the method concurrently, confirming that the asynchronous pool is working and that the controller thread finishes quickly.
To make the pool state visible, the author creates a subclass VisiableThreadPoolTaskExecutor that overrides execute , submit and related methods, calling a helper showThreadPoolInfo which logs task count, completed task count, active thread count and queue size each time a task is submitted.
public class VisiableThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {
private void showThreadPoolInfo(String prefix) {
ThreadPoolExecutor tp = getThreadPoolExecutor();
if (tp == null) return;
logger.info("{}, {}, taskCount [{}], completedTaskCount [{}], activeCount [{}], queueSize [{}]",
getThreadNamePrefix(), prefix,
tp.getTaskCount(), tp.getCompletedTaskCount(), tp.getActiveCount(), tp.getQueue().size());
}
@Override public void execute(Runnable task) { showThreadPoolInfo("1. do execute"); super.execute(task); }
@Override public void execute(Runnable task, long startTimeout) { showThreadPoolInfo("2. do execute"); super.execute(task, startTimeout); }
@Override public Future
submit(Runnable task) { showThreadPoolInfo("1. do submit"); return super.submit(task); }
@Override public
Future
submit(Callable
task) { showThreadPoolInfo("2. do submit"); return super.submit(task); }
// other overrides omitted for brevity
}The original ExecutorConfig bean is then modified to instantiate VisiableThreadPoolTaskExecutor instead of the plain executor, so every task submission logs detailed pool statistics. Sample logs illustrate how the pool reports zero active threads and an empty queue after each task completes, giving developers clear insight into the executor’s behavior.
Overall, the article provides a complete, reproducible guide for setting up asynchronous processing in a Spring Boot application, customizing the thread pool, and monitoring its runtime state.
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.
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.