Why Misusing Spring @Async Can Crash Your System – and How to Avoid It
This article reveals how creating a new thread pool for each @Async method in Spring can exhaust system resources, explains Spring Boot’s default executor behavior, shows code examples, and offers practical guidance to prevent thread‑pool related crashes in Java backend applications.
Long ago I experienced a painful fault caused by reckless thread‑pool usage: a teammate enabled a "brutal" thread‑pool mode, creating a separate pool for every method call. When request volume rises, the operating system is overwhelmed and all business logic becomes unresponsive.
1. Spring's Asynchronous Code
Spring, the beloved Java framework, offers the @Async annotation for easy async execution. Yet many developers blindly apply it, ignoring the multithreading complexities it introduces.
Below is a minimal Spring Boot project that enables async support:
@SpringBootApplication<br/>@EnableAsync<br/>public class DemoApplication {<br/> // ...<br/>}Define a component with an @Async method:
@Component<br/>public class AsyncService {<br/> @Async<br/> public void async() {<br/> try {<br/> Thread.sleep(1000);<br/> System.out.println(Thread.currentThread());<br/> } catch (Exception ex) {<br/> ex.printStackTrace();<br/> }<br/> }<br/>}Create a test endpoint that triggers the async method:
@ResponseBody<br/>@GetMapping("test")<br/>public void test() {<br/> service.async();<br/>}When the endpoint is called, a breakpoint reveals the thread pool used by the async task.
The default async executor uses a ThreadPoolTaskExecutor with corePoolSize=8 and an unbounded LinkedBlockingQueue. Tasks beyond eight threads are queued indefinitely, leading to massive backlog, severe latency, or even OOM errors under high load.
throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, var4);Specifying a custom ThreadPoolExecutor can mitigate the issue, but many teams overlook this.
2. Spring Boot Saves You
Spring Boot’s TaskExecutionAutoConfiguration automatically provides a ThreadPoolTaskExecutor bean, preventing the need to configure one manually.
@ConditionalOnMissingBean({Executor.class})<br/>public ThreadPoolTaskExecutor applicationTaskExecutor(TaskExecutorBuilder builder) {<br/> return builder.build();<br/>}If Boot is absent, Spring falls back to SimpleAsyncTaskExecutor, which creates a brand‑new thread for every task.
@Override<br/>@Nullable<br/>protected Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) {<br/> Executor defaultExecutor = super.getDefaultExecutor(beanFactory);<br/> return (defaultExecutor != null ? defaultExecutor : new SimpleAsyncTaskExecutor());<br/>}The design of SimpleAsyncTaskExecutor is dangerous: each execution spawns a separate thread, so a TPS of 1000 would generate 1000 threads per second, quickly exhausting the OS.
protected void doExecute(Runnable task) {<br/> Thread thread = (this.threadFactory != null ? this.threadFactory.newThread(task) : createThread(task));<br/> thread.start();<br/>}3. End
Using a new‑thread‑per‑task approach is hazardous. Many Spring components, such as AsyncRestTemplate, still rely on SimpleAsyncTaskExecutor, exposing applications to uncontrolled thread creation and potential crashes.
It is strongly recommended to blacklist SimpleAsyncTaskExecutor in your projects and replace it with a properly configured thread pool.
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.
macrozheng
Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.
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.
