第十七课实战:线程池与异步体系实战——SpringBoot 最小异步任务 Demo(完整可运行版)

上一篇我们讲了"线程池与异步"的原理,这篇直接上手:写一个最小可跑的 Spring Boot 异步 Demo

目标很明确:

调用接口立即返回,但后台任务继续跑(线程池执行)。

1. 项目目标与效果

实现接口:

  • GET /register

效果:

  • 接口秒返回注册成功
  • 控制台稍后输出(模拟发邮件/写日志等慢任务):
    • 开始发邮件...
    • 邮件发送完成...

2. 工程目录结构

复制代码
async-demo
├── pom.xml
└── src
    └── main
        ├── java
        │   └── com/example/asyncdemo
        │       ├── AsyncDemoApplication.java
        │       ├── config
        │       │   ├── AsyncPoolProperties.java
        │       │   └── ThreadPoolConfig.java
        │       ├── controller
        │       │   └── UserController.java
        │       └── service
        │           └── AsyncService.java
        └── resources
            └── application.yml

3. pom.xml(最小依赖)

只要一个 Web 依赖就够了(因为我们就写个接口 + 异步)

XML 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>async-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>async-demo</name>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.6</version>
        <relativePath/>
    </parent>

    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <!-- Web:提供 Controller / Tomcat 等 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 可选:仅用于开发热更新 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- 打包运行 -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

说明:我用的是 Spring Boot 3.x + Java 17(当前企业非常常见)。如果你是 2.x 也能跑,写法基本一致。

  1. application.yml(把线程池参数外置)
XML 复制代码
server:
  port: 8080

# 自定义线程池参数(我们自己定义配置前缀 async.pool)
async:
  pool:
    core-size: 5
    max-size: 10
    queue-capacity: 50
    keep-alive-seconds: 60
    thread-name-prefix: async-

为什么要放 yml?

  • 线程池参数经常需要调(压测后调整)
  • 不用改代码就能调参
  • 工程习惯就是"参数外置"

5. 启动类:开启异步能力

AsyncDemoApplication.java

java 复制代码
package com.example.asyncdemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@EnableAsync
@SpringBootApplication
public class AsyncDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(AsyncDemoApplication.class, args);
    }
}

关键点:

  • @EnableAsync:不加它,@Async 不会生效

6. 配置属性类:把 yml 读成对象

AsyncPoolProperties.java

java 复制代码
package com.example.asyncdemo.config;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "async.pool")
public class AsyncPoolProperties {

    private int coreSize = 5;
    private int maxSize = 10;
    private int queueCapacity = 50;
    private int keepAliveSeconds = 60;
    private String threadNamePrefix = "async-";

    public int getCoreSize() {
        return coreSize;
    }

    public void setCoreSize(int coreSize) {
        this.coreSize = coreSize;
    }

    public int getMaxSize() {
        return maxSize;
    }

    public void setMaxSize(int maxSize) {
        this.maxSize = maxSize;
    }

    public int getQueueCapacity() {
        return queueCapacity;
    }

    public void setQueueCapacity(int queueCapacity) {
        this.queueCapacity = queueCapacity;
    }

    public int getKeepAliveSeconds() {
        return keepAliveSeconds;
    }

    public void setKeepAliveSeconds(int keepAliveSeconds) {
        this.keepAliveSeconds = keepAliveSeconds;
    }

    public String getThreadNamePrefix() {
        return threadNamePrefix;
    }

    public void setThreadNamePrefix(String threadNamePrefix) {
        this.threadNamePrefix = threadNamePrefix;
    }
}

7. 线程池配置类:真正创建线程池 Bean

ThreadPoolConfig.java

java 复制代码
package com.example.asyncdemo.config;

import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@Configuration
@EnableConfigurationProperties(AsyncPoolProperties.class)
public class ThreadPoolConfig {

    /**
     * 给 @Async 指定的线程池(名字要对应 @Async("taskExecutor"))
     */
    @Bean(name = "taskExecutor")
    public Executor taskExecutor(AsyncPoolProperties props) {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(props.getCoreSize());
        executor.setMaxPoolSize(props.getMaxSize());
        executor.setQueueCapacity(props.getQueueCapacity());
        executor.setKeepAliveSeconds(props.getKeepAliveSeconds());
        executor.setThreadNamePrefix(props.getThreadNamePrefix());

        // 这个策略很重要:队列满了 + 线程也到上限时,让调用方线程自己执行(削峰,避免直接丢任务)
        executor.setRejectedExecutionHandler(new java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy());

        executor.initialize();
        return executor;
    }
}

这里我额外加了一个"工程必备"的点:

  • CallerRunsPolicy():线程池打满时不直接丢任务,而是让请求线程自己执行,用来削峰(你后面学限流/降级会更懂它的价值)

8. 异步业务类:@Async 真正起作用的地方

AsyncService.java

java 复制代码
package com.example.asyncdemo.service;

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class AsyncService {

    /**
     * 注意:必须 public
     * 注意:不要在同一个类里 self-invoke 调用(否则 @Async 失效)
     */
    @Async("taskExecutor")
    public void sendWelcomeEmail(String username) {
        try {
            System.out.println("【异步任务】开始发邮件,用户=" + username +
                    ",线程=" + Thread.currentThread().getName());

            // 模拟耗时 3 秒
            Thread.sleep(3000);

            System.out.println("【异步任务】邮件发送完成,用户=" + username +
                    ",线程=" + Thread.currentThread().getName());
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.out.println("【异步任务】被中断,用户=" + username);
        }
    }

    @Async("taskExecutor")
    public void writeAuditLog(String action) {
        try {
            System.out.println("【异步任务】开始写日志,action=" + action +
                    ",线程=" + Thread.currentThread().getName());

            Thread.sleep(1500);

            System.out.println("【异步任务】日志写入完成,action=" + action +
                    ",线程=" + Thread.currentThread().getName());
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.out.println("【异步任务】日志写入被中断,action=" + action);
        }
    }
}

我这里放了两个异步任务,让你更直观看到:

  • 一个接口触发多个后台任务
  • 都跑在线程池线程里(async- 前缀)

9. Controller:接口秒返回,但触发异步任务

UserController.java

java 复制代码
package com.example.asyncdemo.controller;

import com.example.asyncdemo.service.AsyncService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;

@RestController
public class UserController {

    private final AsyncService asyncService;

    public UserController(AsyncService asyncService) {
        this.asyncService = asyncService;
    }

    @GetMapping("/register")
    public String register() {
        String username = "lingpei";
        String action = "register@" + LocalDateTime.now();

        // 触发异步任务(不阻塞接口返回)
        asyncService.sendWelcomeEmail(username);
        asyncService.writeAuditLog(action);

        // 立即返回
        return "注册成功(接口秒返回),后台任务执行中...";
    }
}

10. 运行验证(你照着做就能看到效果)

启动:

java 复制代码
mvn spring-boot:run

请求:

java 复制代码
curl "http://localhost:8080/register"

你会看到:

  • curl 立刻返回:注册成功...
  • 控制台输出类似:
java 复制代码
【异步任务】开始发邮件... 线程=async-1
【异步任务】开始写日志... 线程=async-2
【异步任务】日志写入完成...
【异步任务】邮件发送完成...

这就证明:

  • Controller 没等任务执行完就返回了
  • 任务在你定义的线程池里跑

11. 常见坑(@Async 失效 99% 都是这几个)

坑 1:没加 @EnableAsync

✅ 解决:启动类加上。

坑 2:异步方法不是 public

✅ 解决:必须 public。

坑 3:同类内部调用(self-invoke)

例如:

java 复制代码
this.sendWelcomeEmail();

✅ 解决:必须通过 Spring 注入的代理对象调用(也就是通过别的 Bean 调用它)。

坑 4:没指定线程池,默认线程池不可控

✅ 解决:自定义 ThreadPoolTaskExecutor,并用 @Async("taskExecutor") 指定。

12. 一句话总结

  • 线程池:控制并发资源,避免线程爆炸

  • 异步:把慢任务从主流程挪走,让接口秒返回

  • 工程关键点:线程池参数外置 + 拒绝策略 + 避坑点

到这里,你已经具备后端并发的"最小实战闭环"。

下一篇:

Spring 异步与线程池实战全解:失效原因、参数调优与 MQ 选型

相关推荐
寻寻觅觅☆11 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
l1t11 小时前
在wsl的python 3.14.3容器中使用databend包
开发语言·数据库·python·databend
青云计划12 小时前
知光项目知文发布模块
java·后端·spring·mybatis
赶路人儿12 小时前
Jsoniter(java版本)使用介绍
java·开发语言
ceclar12312 小时前
C++使用format
开发语言·c++·算法
探路者继续奋斗13 小时前
IDD意图驱动开发之意图规格说明书
java·规格说明书·开发规范·意图驱动开发·idd
码说AI13 小时前
python快速绘制走势图对比曲线
开发语言·python
Gofarlic_OMS13 小时前
科学计算领域MATLAB许可证管理工具对比推荐
运维·开发语言·算法·matlab·自动化
星空下的月光影子13 小时前
易语言开发从入门到精通:补充篇·网络爬虫与自动化采集分析系统深度实战·HTTP/HTTPS请求·HTML/JSON解析·反爬策略·电商价格监控·新闻资讯采集
开发语言
老约家的可汗13 小时前
初识C++
开发语言·c++