Avoid Out‑of‑Memory Errors: Properly Configure Spring Boot Async Thread Pool

This article explains why using @Async in Spring Boot can cause memory overflow when many requests trigger parallel tasks, examines the default thread‑pool settings that lead to unbounded queues, and provides step‑by‑step configuration of a safe thread pool to prevent out‑of‑memory failures.

Programmer DD
Programmer DD
Programmer DD
Avoid Out‑of‑Memory Errors: Properly Configure Spring Boot Async Thread Pool

In the previous article we introduced how to use the @Async annotation to create asynchronous tasks in Spring Boot. While this can speed up execution, invoking the endpoint frequently creates a large number of async tasks, which may lead to memory overflow because the default thread pool uses an unbounded queue and maximum thread count.

@RestController
public class HelloController {
    @Autowired
    private AsyncTasks asyncTasks;

    @GetMapping("/hello")
    public String hello() {
        // Split parallelizable logic into three async tasks
        CompletableFuture<String> task1 = asyncTasks.doTaskOne();
        CompletableFuture<String> task2 = asyncTasks.doTaskTwo();
        CompletableFuture<String> task3 = asyncTasks.doTaskThree();

        CompletableFuture.allOf(task1, task2, task3).join();
        return "Hello World";
    }
}

When a single request calls this endpoint there is no problem, but with many concurrent requests the number of async tasks grows to 3 × n (where n is the request count). If task processing is not fast enough, memory overflow occurs. The root cause is the default Spring Boot async thread‑pool configuration, which sets queueCapacity and maxSize to Integer.MAX_VALUE (2³¹‑1).

The two critical parameters to watch are: queueCapacity: capacity of the buffer queue, default is Integer.MAX_VALUE. maxSize: maximum number of threads, also default Integer.MAX_VALUE.

Configuring the Default Thread Pool

Adjust the thread‑pool settings in application.properties (or application.yml) to prevent memory overflow:

spring.task.execution.pool.core-size=2
spring.task.execution.pool.max-size=5
spring.task.execution.pool.queue-capacity=10
spring.task.execution.pool.keep-alive=60s
spring.task.execution.pool.allow-core-thread-timeout=true
spring.task.execution.thread-name-prefix=task-

The meaning of each property: spring.task.execution.pool.core-size: initial number of threads when the pool is created (default 8). spring.task.execution.pool.max-size: maximum number of threads (default Integer.MAX_VALUE). spring.task.execution.pool.queue-capacity: size of the task buffer queue (default Integer.MAX_VALUE). spring.task.execution.pool.keep-alive: idle time before a thread is terminated. spring.task.execution.pool.allow-core-thread-timeout: whether core threads are allowed to time out. spring.task.execution.shutdown.await-termination: whether to wait for remaining tasks before shutting down. spring.task.execution.shutdown.await-termination-period: maximum wait time for remaining tasks. spring.task.execution.thread-name-prefix: prefix for thread names, useful for log tracing.

Hands‑On Test

First run a unit test without custom thread‑pool configuration:

@Test
public void test1() throws Exception {
    long start = System.currentTimeMillis();

    CompletableFuture<String> task1 = asyncTasks.doTaskOne();
    CompletableFuture<String> task2 = asyncTasks.doTaskTwo();
    CompletableFuture<String> task3 = asyncTasks.doTaskThree();

    CompletableFuture.allOf(task1, task2, task3).join();

    long end = System.currentTimeMillis();
    log.info("任务全部完成,总耗时:" + (end - start) + "毫秒");
}

Log output (core pool size 8, so all three tasks start simultaneously):

2021-09-15 00:30:14.819  INFO 77614 --- [task-2] com.didispace.chapter76.AsyncTasks       : 开始做任务二
2021-09-15 00:30:14.819  INFO 77614 --- [task-3] com.didispace.chapter76.AsyncTasks       : 开始做任务三
2021-09-15 00:30:14.819  INFO 77614 --- [task-1] com.didispace.chapter76.AsyncTasks       : 开始做任务一
2021-09-15 00:30:15.491  INFO 77614 --- [task-2] com.didispace.chapter76.AsyncTasks       : 完成任务二,耗时:672毫秒
2021-09-15 00:30:19.496  INFO 77614 --- [task-3] com.didispace.chapter76.AsyncTasks       : 完成任务三,耗时:4677毫秒
2021-09-15 00:30:20.443  INFO 77614 --- [task-1] com.didispace.chapter76.AsyncTasks       : 完成任务一,耗时:5624毫秒
2021-09-15 00:30:20.443  INFO 77614 --- [main] c.d.chapter76.Chapter76ApplicationTests  : 任务全部完成,总耗时:5653毫秒

Now add the custom thread‑pool configuration shown above and rerun the test:

spring.task.execution.pool.core-size=2
spring.task.execution.pool.max-size=5
spring.task.execution.pool.queue-capacity=10
spring.task.execution.pool.keep-alive=60s
spring.task.execution.pool.allow-core-thread-timeout=true
spring.task.execution.thread-name-prefix=task-

Log output changes to:

2021-09-15 00:31:50.013  INFO 77985 --- [task-1] com.didispace.chapter76.AsyncTasks       : 开始做任务一
2021-09-15 00:31:50.013  INFO 77985 --- [task-2] com.didispace.chapter76.AsyncTasks       : 开始做任务二
2021-09-15 00:31:52.452  INFO 77985 --- [task-1] com.didispace.chapter76.AsyncTasks       : 完成任务一,耗时:2439毫秒
2021-09-15 00:31:52.452  INFO 77985 --- [task-1] com.didispace.chapter76.AsyncTasks       : 开始做任务三
2021-09-15 00:31:55.880  INFO 77985 --- [task-2] com.didispace.chapter76.AsyncTasks       : 完成任务二,耗时:5867毫秒
2021-09-15 00:32:00.346  INFO 77985 --- [task-1] com.didispace.chapter76.AsyncTasks       : 完成任务三,耗时:7894毫秒
2021-09-15 00:32:00.347  INFO 77985 --- [main] c.d.chapter76.Chapter76ApplicationTests  : 任务全部完成,总耗时:10363毫秒

Observation: tasks one and two immediately occupy the two core threads; task three waits in the queue. When task one finishes, its core thread becomes free, pulls task three from the queue, and starts processing it.

Note: Although max-size is set to 5, task three does not create a new thread because the queue is not full. Only when the queue reaches its capacity (10 in this example) will additional threads beyond the core size be created.

For the complete project, see the chapter7-6 module under the 2.x directory of the repository:

GitHub: https://github.com/dyc87112/SpringBoot-Learning/

Gitee: https://gitee.com/didispace/SpringBoot-Learning/

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.

JavaperformanceMemory ManagementSpring Bootthread poolAsync
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.