Isolating Spring @Async Thread Pools to Prevent Task Interference

This tutorial explains why the default shared thread pool for @Async tasks can cause unrelated services to block each other, and shows step‑by‑step how to configure separate thread pools for different async tasks in Spring Boot, complete with code examples and a unit test.

Programmer DD
Programmer DD
Programmer DD
Isolating Spring @Async Thread Pools to Prevent Task Interference

In the previous article we introduced how @Async tasks are executed by a thread pool. To avoid excessive resource usage we must configure the pool, but when multiple independent services share the same pool, a slow task in one service can block the other.

What is thread‑pool isolation and why is it needed?

When several asynchronous tasks use the default shared pool, a performance problem in one task can affect all other tasks because they compete for the same threads.

@RestController
public class HelloController {

    @Autowired
    private AsyncTasks asyncTasks;

    @GetMapping("/api-1")
    public String taskOne() {
        CompletableFuture<String> task1 = asyncTasks.doTaskOne("1");
        CompletableFuture<String> task2 = asyncTasks.doTaskOne("2");
        CompletableFuture<String> task3 = asyncTasks.doTaskOne("3");
        CompletableFuture.allOf(task1, task2, task3).join();
        return "";
    }

    @GetMapping("/api-2")
    public String taskTwo() {
        CompletableFuture<String> task1 = asyncTasks.doTaskTwo("1");
        CompletableFuture<String> task2 = asyncTasks.doTaskTwo("2");
        CompletableFuture<String> task3 = asyncTasks.doTaskTwo("3");
        CompletableFuture.allOf(task1, task2, task3).join();
        return "";
    }
}

Both APIs split their work into three asynchronous tasks. If the thread pool’s maximum size is small (e.g., 2), slow tasks from one API can occupy all threads, causing the other API to block.

Configure different thread pools for different async tasks

Step 1 – Initialize multiple thread pools:

@EnableAsync
@Configuration
public class TaskPoolConfig {

    @Bean
    public Executor taskExecutor1() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(2);
        executor.setQueueCapacity(10);
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("executor-1-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return executor;
    }

    @Bean
    public Executor taskExecutor2() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(2);
        executor.setQueueCapacity(10);
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("executor-2-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return executor;
    }
}
Note: Setting executor.setThreadNamePrefix makes it easy to see which pool a thread belongs to.

Step 2 – Create async tasks and bind them to the pools:

@Slf4j
@Component
public class AsyncTasks {

    public static Random random = new Random();

    @Async("taskExecutor1")
    public CompletableFuture<String> doTaskOne(String taskNo) throws Exception {
        log.info("开始任务:{}", taskNo);
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        log.info("完成任务:{},耗时:{} 毫秒", taskNo, end - start);
        return CompletableFuture.completedFuture("任务完成");
    }

    @Async("taskExecutor2")
    public CompletableFuture<String> doTaskTwo(String taskNo) throws Exception {
        log.info("开始任务:{}", taskNo);
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        log.info("完成任务:{},耗时:{} 毫秒", taskNo, end - start);
        return CompletableFuture.completedFuture("任务完成");
    }
}

The names taskExecutor1 and taskExecutor2 correspond to the bean methods defined in the configuration class.

Step 3 – Write a unit test to verify isolation:

@Slf4j
@SpringBootTest
public class Chapter77ApplicationTests {

    @Autowired
    private AsyncTasks asyncTasks;

    @Test
    public void test() throws Exception {
        long start = System.currentTimeMillis();
        // pool 1
        CompletableFuture<String> task1 = asyncTasks.doTaskOne("1");
        CompletableFuture<String> task2 = asyncTasks.doTaskOne("2");
        CompletableFuture<String> task3 = asyncTasks.doTaskOne("3");
        // pool 2
        CompletableFuture<String> task4 = asyncTasks.doTaskTwo("4");
        CompletableFuture<String> task5 = asyncTasks.doTaskTwo("5");
        CompletableFuture<String> task6 = asyncTasks.doTaskTwo("6");
        // run all
        CompletableFuture.allOf(task1, task2, task3, task4, task5, task6).join();
        long end = System.currentTimeMillis();
        log.info("任务全部完成,总耗时:" + (end - start) + "毫秒");
    }
}

The test launches six async tasks: three use pool 1 and three use pool 2. With each pool configured with a core size of 2, the expected execution order is:

In pool 1, task1 and task2 start immediately; task3 waits in the queue.

In pool 2, task4 and task5 start immediately; task6 waits in the queue.

When any running task finishes, the queued task (task3 or task6) is picked up.

Running the test produces logs similar to the following, confirming that tasks from different pools do not block each other:

2021-09-15 23:45:11.369  INFO 61670 --- [   executor-1-1] com.didispace.chapter77.AsyncTasks       : 开始任务:1
2021-09-15 23:45:11.369  INFO 61670 --- [   executor-2-2] com.didispace.chapter77.AsyncTasks       : 开始任务:5
2021-09-15 23:45:11.369  INFO 61670 --- [   executor-2-1] com.didispace.chapter77.AsyncTasks       : 开始任务:4
2021-09-15 23:45:11.369  INFO 61670 --- [   executor-1-2] com.didispace.chapter77.AsyncTasks       : 开始任务:2
... (log continues) ...
2021-09-15 23:45:24.117  INFO 61670 --- [   main] c.d.chapter77.Chapter77ApplicationTests  : 任务全部完成,总耗时:12762毫秒

By isolating @Async tasks into dedicated thread pools, you prevent a slow or blocked task in one service from degrading the performance of unrelated services.

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.

JavaconcurrencySpring Bootthread poolAsyncIsolation
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

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.