java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Spring Boot @Async线程池

深度解析Spring Boot @Async 正在榨干服务器资源

作者:Omari7

这篇文章主要介绍了Spring Boot @Async 正在榨干服务器资源,我们就来彻底搞懂 @Async 背后的线程池,并学会如何像个老手一样驾驭它,让你的应用在高并发下稳如泰山,需要的朋友可以参考下

“服务又挂了?日志里全是 OutOfMemoryError: unable to create new native thread!”

你是否也曾深夜被这样的报警惊醒?明明只是一个简单的异步处理,用 @Async 注解轻轻一标,本地测试跑得飞快,一到高并发压测就瞬间“猝死”。这几乎是每个从初级走向中级的 Java 开发者都会踩的坑。

问题出在哪里?出在你以为 @Async 是“银弹”,但实际上,你只是把它当成了一把没加防护的“电锯”在用。今天,我们就来彻底搞懂 @Async 背后的线程池,并学会如何像个老手一样驾驭它,让你的应用在高并发下稳如泰山。

核心概念拆解:把线程池当成一个“智慧厨房”

忘掉那些枯燥的 API 定义,我们用一个更形象的方式来理解线程池的核心思想。

想象一下,你开了一家生意火爆的餐厅。

方案一:裸奔模式 (不使用线程池) 每来一位顾客,你就临时去人才市场招一位新厨师。顾客少的时候还行,一旦到了饭点高峰期,几百个顾客同时涌入,你的小厨房瞬间塞满了临时厨师,乱作一团,最终厨房(服务器内存)被挤爆,餐厅(服务)直接关门大吉。这就是 unable to create new native thread 的本质——系统资源被无限创建的线程耗尽了。

方案二:智慧厨房模式 (使用线程池) 你学聪明了,建立了一个专业的厨师团队(线程池)。

  1. 核心厨师 (corePoolSize):你有 5 位正式厨师,他们是厨房的中坚力量,时刻准备接单。
  2. 等餐区 (workQueue):当 5 位厨师都在忙时,新来的订单会先放到一个等餐区(任务队列)排队。
  3. 临时工 (maximumPoolSize):如果等餐区的订单也排满了(比如超过 100 单),说明生意实在太火爆了。你决定临时再请 3 位厨师来帮忙,这样厨房最多就有 8 位厨师。
  4. 拒绝策略 (RejectedExecutionHandler):如果 8 位厨师全在忙,等餐区也满了,再有新顾客来,服务员(拒绝策略)就会礼貌地告诉他:“抱歉,今天太忙了,您稍后再来吧。” 而不是让顾客无止境地等下去,最终导致体验崩溃。
  5. 临时工的补贴 (keepAliveTime):高峰期过后,那 3 位临时工如果连续 1 分钟都没接到新活,你就会让他们下班,以节省成本(释放空闲线程资源),只保留 5 位核心厨师。

看到了吗?一个设计良好的线程池,就像一个管理有序的智慧厨房,它通过复用排队限流,确保了在任何客流量下,厨房都能高效、稳定地运转,而不是无限扩张导致崩溃。

实战落地:在 Spring Boot 中正确“驯服”@Async

光说不练假把式。我们来看如何在 Spring Boot 项目中,从“裸奔”模式进化到“精细化”管理模式。

1. 依赖(Maven)

你只需要 Spring Boot 的 Web 启动器,@Async 的支持是内置的。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.7.5</version> <!-- 请根据你的项目选择合适的版本 -->
</dependency>

2. 启用异步功能

在你的主启动类上添加 @EnableAsync 注解。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync // 开启异步任务支持
public class HighConcurrencyApplication {
    public static void main(String[] args) {
        SpringApplication.run(HighConcurrencyApplication.class, args);
    }
}

3. 错误的用法(裸奔的@Async)

很多新手会直接在一个 Service 方法上标注 @Async,就像这样:

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
    @Async // 警告:这是默认配置,高并发下极度危险!
    public void processOrder(String orderId) {
        // 模拟处理订单的耗时操作
        System.out.println(Thread.currentThread().getName() + " 开始处理订单: " + orderId);
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 订单处理完成: " + orderId);
    }
}

为什么危险? 因为在没有自定义线程池的情况下,Spring Boot 会使用一个 SimpleAsyncTaskExecutor。这个执行器不会复用线程,每次调用都会创建一个新线程。这和我们“智慧厨房”的反面教材——方案一,一模一样。

4. 正确的姿势:自定义线程池

我们需要创建一个配置类,来定义我们自己的“智慧厨房”。

ThreadPoolConfig.java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
public class ThreadPoolConfig {
    // Bean 的名称,我们将在 @Async 注解中引用它
    public static final String ASYNC_EXECUTOR_NAME = "myAsyncExecutor";
    @Bean(name = ASYNC_EXECUTOR_NAME)
    public Executor myAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 核心厨师数量:根据CPU核心数动态计算
        int corePoolSize = Runtime.getRuntime().availableProcessors();
        executor.setCorePoolSize(corePoolSize); 
        // 最大厨师数量:通常是核心数的2-3倍
        executor.setMaxPoolSize(corePoolSize * 2);
        // 等餐区容量
        executor.setQueueCapacity(100); 
        // 临时工的存活时间
        executor.setKeepAliveSeconds(60); 
        // 给厨师起个好名字,方便排查问题
        executor.setThreadNamePrefix("My-Async-"); 
        // 拒绝策略:让调用者自己处理,这是最稳妥的方式
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 初始化
        executor.initialize();
        return executor;
    }
}

application.yml (推荐)

为了灵活性,最好将核心参数配置在 application.yml 中。

myapp:
  async:
    core-pool-size: 8
    max-pool-size: 16
    queue-capacity: 200
    keep-alive-seconds: 60

然后在配置类中通过 @Value 注入这些值。

5. 在业务代码中指定线程池

现在,让我们的 OrderService 使用新定义的“智慧厨房”。

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import com.yourpackage.config.ThreadPoolConfig; // 引入你的配置类
@Service
public class OrderService {
    // 通过 value 属性指定要使用的线程池 Bean 名称
    @Async(ThreadPoolConfig.ASYNC_EXECUTOR_NAME)
    public void processOrder(String orderId) {
        // ... 业务逻辑不变 ...
        System.out.println("使用自定义线程池 [" + Thread.currentThread().getName() + "] 开始处理订单: " + orderId);
        // ...
    }
}

现在,所有的异步订单处理任务都会被提交到我们精心设计的 myAsyncExecutor 线程池中,再也不怕野线程满天飞了。

避坑指南:生产环境的“秘密”

总结与升华

我们今天从一个常见的 OOM 惨案出发,通过一个“智慧厨房”的比喻,彻底理解了线程池的工作原理。核心的收获是:

永远不要在生产环境中使用默认的 @Async,必须为其提供一个经过精细化配置的自定义线程池。

这不仅是代码技巧,更是一种对系统负责的架构思维。我们从“能用”走向了“可靠、可控”。

最后,留一个思考题:我们今天讨论的都是单个应用内的线程池。如果我们的系统是分布式的微服务架构,一个请求需要跨越多个服务,我们又该如何控制整个链路的并发和资源,防止上游的流量洪峰打垮下游所有服务呢?

到此这篇关于Spring Boot @Async 正在榨干服务器资源的文章就介绍到这了,更多相关Spring Boot @Async服务器资源内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文