Mastering Spring Async Tasks: Boost Performance with Custom Thread Pools

This article explains why and how to use Spring's asynchronous tasks, demonstrates enabling @Async, shows the default SimpleAsyncTaskExecutor behavior, and guides you through customizing a thread pool to improve application throughput and responsiveness.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Mastering Spring Async Tasks: Boost Performance with Custom Thread Pools

Environment: Spring 5.3.23

1. Introduction

Asynchronous tasks in Spring allow time‑consuming operations to run in the background, preventing the main thread from blocking and improving concurrency, performance, user experience, resource utilization, system load, and handling of high‑traffic scenarios.

2. Practical Code

Enable async support with @EnableAsync in a configuration class:

@Configuration
@EnableAsync
static class AppConfig {}

Create a component with an @Async method:

@Component
static class AsyncService {
    // This method will be executed in a separate thread
    @Async
    public void calc() {
        System.out.printf("Executing thread: %s - start%n", Thread.currentThread().getName());
        try {
            // Simulate a time‑consuming operation
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.printf("Thread: %s - finished%n", Thread.currentThread().getName());
    }
}

Test the async service:

try (GenericApplicationContext context = new GenericApplicationContext()) {
    context.registerBean(ConfigurationClassPostProcessor.class);
    context.registerBean(AppConfig.class);
    context.registerBean(AsyncService.class);
    context.refresh();
    AsyncService as = context.getBean(AsyncService.class);
    as.calc();
    as.calc();
    as.calc();
    System.out.println("Main thread ends...");
    System.in.read();
}

Sample output demonstrates that the main thread finishes early while three separate worker threads execute the tasks:

Main thread ends...
Executing thread: SimpleAsyncTaskExecutor-1 - start
Executing thread: SimpleAsyncTaskExecutor-2 - start
Executing thread: SimpleAsyncTaskExecutor-3 - start
Thread: SimpleAsyncTaskExecutor-2 - finished
Thread: SimpleAsyncTaskExecutor-1 - finished
Thread: SimpleAsyncTaskExecutor-3 - finished

3. Thread Pool Used by Spring Async

Spring implements async execution via AOP. The AsyncAnnotationBeanPostProcessor is created when @EnableAsync is processed. It registers an AsyncAnnotationAdvisor that builds an interceptor ( AnnotationAsyncExecutionInterceptor) which ultimately determines the executor.

The executor resolution follows these steps:

Look for a bean implementing AsyncConfigurer in the container.

If none is found, search for a single TaskExecutor bean; if multiple exist, look for a bean named taskExecutor of type Executor.

If still not found, attempt to retrieve a bean named taskExecutor of type Executor.

If all the above fail, Spring creates a default SimpleAsyncTaskExecutor.

The relevant source classes include ProxyAsyncConfiguration, AbstractAsyncConfiguration, AsyncAnnotationBeanPostProcessor, AsyncAnnotationAdvisor, AsyncExecutionInterceptor, and AsyncExecutionAspectSupport. The default executor is obtained in AsyncExecutionAspectSupport#getDefaultExecutor, which falls back to SimpleAsyncTaskExecutor when no user‑defined executor is present.

4. Customizing the Thread Pool

To provide a custom executor, implement AsyncConfigurer and register it as a bean:

@Component
static class CustomAsyncConfigurer implements AsyncConfigurer {
    @Override
    public Executor getAsyncExecutor() {
        return new ThreadPoolExecutor(
            2, 2, 60, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(),
            new ThreadFactory() {
                private final AtomicInteger poolNumber = new AtomicInteger(1);
                private final ThreadGroup group = Thread.currentThread().getThreadGroup();
                private final AtomicInteger threadNumber = new AtomicInteger(1);
                private final String namePrefix = "pack-" + poolNumber.getAndIncrement() + "-thread-";
                public Thread newThread(Runnable r) {
                    Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
                    if (t.isDaemon()) t.setDaemon(false);
                    if (t.getPriority() != Thread.NORM_PRIORITY) t.setPriority(Thread.NORM_PRIORITY);
                    return t;
                }
            }
        );
    }
}

After registering this bean, the output shows the custom thread names, confirming that the custom pool is in effect.

Main thread ends...
Executing thread: pack-1-thread-1 - start
Executing thread: pack-1-thread-2 - start
Thread: pack-1-thread-2 - finished
Thread: pack-1-thread-1 - finished
Executing thread: pack-1-thread-2 - start
Thread: pack-1-thread-2 - finished

In summary, Spring's async support provides a simple way to improve performance, and developers can either rely on the default SimpleAsyncTaskExecutor or supply a tailored Executor via AsyncConfigurer to meet specific concurrency requirements.

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.

JavaperformancespringThreadPool
Spring Full-Stack Practical Cases
Written by

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.

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.