【SpringBoot】业务多线程四大实战场景

SpringBoot 业务多线程四大实战场景(全网超详细落地总结)

一、前言

很多同学学习多线程,只会背 Thread、线程池原理、锁机制,但是完全不知道在真实业务中怎么用、什么时候用、用哪种方案

在真实 CRUD 业务开发中,多线程并不是用来"炫技",而是为了:

  • 缩短接口响应时间

  • 并行处理多个无依赖任务

  • 大批量数据提速处理

  • 规避线程安全线上事故

本文基于 SpringBoot + MyBatis-Plus 三层架构 ,从零落地 4 套企业级业务多线程场景,包含:

  1. @Async 无返回异步(日志/通知)

  2. @Async + Future 并行查询汇总统计

  3. 手动自定义线程池 + 分片批量导入(execute / submit / CompletableFuture)

  4. Spring 单例线程安全深坑(HashMap 并发踩坑 + 彻底解决)

全文包含:完整可运行代码、报错原因、踩坑复盘、原理对比、错误写法、正确规范,是一套可以直接用于面试、工作、毕业设计的标准业务多线程方案。

二、统一前置环境(四个场景通用)

2.1 开启异步支持

启动类必须添加注解,否则 @Async 全部失效:

Plain 复制代码
@SpringBootApplication
@EnableAsync // 开启 Spring 异步线程支持
public class StudyApplication {
    public static void main(String[] args) {
        SpringApplication.run(StudyApplication.class, args);
    }
}

2.2 自定义线程池配置(核心!杜绝默认线程池风险)

生产绝对禁止使用 Spring 默认线程池,必须自定义线程池,区分「轻量异步任务」和「大批量IO任务」。

Plain 复制代码
@Configuration
public class AsyncThreadPoolConfig {

    /**
     * 轻量异步线程池:日志、消息、推送
     */
    @Bean("asyncLogExecutor")
    public ThreadPoolTaskExecutor asyncLogExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(8);
        executor.setMaxPoolSize(20);
        executor.setQueueCapacity(200);
        executor.setThreadNamePrefix("async-log-");
        // 队列满了交给主线程执行,不丢数据
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }

    /**
     * 批量任务线程池:大数据分片导入、数据同步
     */
    @Bean("asyncBatchExecutor")
    public ThreadPoolTaskExecutor asyncBatchExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(30);
        executor.setQueueCapacity(50);
        executor.setThreadNamePrefix("async-batch-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

三、场景一:@Async 无返回异步任务(日志/消息推送)

3.1 业务场景

用户新增/修改后,记录操作日志、推送消息、短信通知。

特点:非核心业务、不需要返回结果、不阻塞主接口

3.2 核心规则(必考面试点)

  • @Async 方法必须抽离到独立 Service

  • 本类调用异步方法会失效(AOP 无法拦截 this 调用)

  • 无返回值异步任务,异常会静默丢失,必须手动 try-catch

3.3 代码实现

异步日志 Service

Plain 复制代码
@Service
public class UserLogAsyncService {

    // 指定使用自定义日志线程池
    @Async("asyncLogExecutor")
    public void saveOperateLog(String username, String operateType) {
        // 必须手动捕获异常,否则异常丢失
        try {
            System.out.println("异步记录操作日志:用户=" + username + ",操作类型=" + operateType);
            // 可扩展:入库、推送、短信
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Controller 调用

Plain 复制代码
@PostMapping("/add")
public String add(@RequestBody SysUser user) {
    // 主业务:新增用户
    sysUserService.save(user);
    // 异步任务:不阻塞主流程
    userLogAsyncService.saveOperateLog(user.getUsername(), "新增用户");
    return "新增成功";
}

3.4 场景总结

  • 适用:日志、消息、短信、非核心后置任务

  • 优点:接口响应极速,主线程不等待

  • 缺点:无法获取任务结果,异常需要自行处理


四、场景二:@Async + Future 并行多任务查询(数据统计)

4.1 业务场景

用户数据统计页面,需要同时查询:

  1. 正常用户列表

  2. 禁用用户列表

  3. 用户总数

三个任务 无依赖关系,可以并行执行,大幅缩短接口耗时。

4.2 核心原理

  • Future:异步任务"提货单"

  • AsyncResult:Spring 封装的 Future 实现类

  • 铁律:先批量提交所有任务,再统一 get(),否则退化为串行

4.3 异步查询 Service

Plain 复制代码
@Service
public class UserQueryAsyncService {

    @Resource
    private SysUserMapper sysUserMapper;

    @Async("asyncLogExecutor")
    public Future<List<SysUser>> getNormalUserList() {
        List<SysUser> list = sysUserMapper.selectList(
                new LambdaQueryWrapper<>().eq(SysUser::getStatus, 1)
        );
        return new AsyncResult<>(list);
    }

    @Async("asyncLogExecutor")
    public Future<List<SysUser>> getDisableUserList() {
        List<SysUser> list = sysUserMapper.selectList(
                new LambdaQueryWrapper<>().eq(SysUser::getStatus, 0)
        );
        return new AsyncResult<>(list);
    }

    @Async("asyncLogExecutor")
    public Future<Long> getUserTotal() {
        Long count = sysUserMapper.selectCount(null);
        return new AsyncResult<>(count);
    }
}

4.4 Controller 正确调用方式

Plain 复制代码
@GetMapping("/userStat")
public UserStatVO userStat() throws ExecutionException, InterruptedException {
    // 1. 全部先提交任务(并行执行)
    Future<List<SysUser>> normalFuture = userQueryAsyncService.getNormalUserList();
    Future<List<SysUser>> disableFuture = userQueryAsyncService.getDisableUserList();
    Future<Long> totalFuture = userQueryAsyncService.getUserTotal();

    // 2. 统一阻塞获取结果
    List<SysUser> normalList = normalFuture.get();
    List<SysUser> disableList = disableFuture.get();
    Long total = totalFuture.get();

    // 3. 组装返回
    UserStatVO vo = new UserStatVO();
    vo.setNormalUserList(normalList);
    vo.setDisableUserList(disableList);
    vo.setUserTotal(total);
    return vo;
}

4.5 严重错误写法(必记避坑)

Plain 复制代码
// 错误!提交一个 get 一个,串行执行
Future<List<SysUser>> future = userQueryAsyncService.getNormalUserList();
List<SysUser> list = future.get();

一旦写完,并行失效,多线程毫无意义。

4.6 耗时对比

  • 串行总耗时 = 任务1 + 任务2 + 任务3

  • 并行总耗时 = 最慢的一个任务耗时


五、场景三:手动线程池分片批量导入(脱离 @Async 注解)

5.1 场景区分

场景1、场景2 都是 注解AOP驱动,适合固定任务。

场景3 是手动操控线程池,适合:

  • 大批量数据导入

  • 循环动态生成任务

  • 需要精细控制线程池

5.2 核心两大提交方式

  1. execute():无返回、不等待、后台执行

  2. submit():有 Future 返回、可等待、可捕获异常

5.3 核心踩坑点(学习过程真实遇到)

坑1:subList 只是视图,多线程并发报错

subList() 不会创建新集合,只是引用原集合,多线程操作极易并发修改异常。

解决方案:手动 new ArrayList 拷贝

Plain 复制代码
List<SysUser> subList = allUserList.subList(i, end);
List<SysUser> saveData = new ArrayList<>(subList);

坑2:Executor 无法调用 submit

顶层接口 Executor 只有 execute 方法,必须注入 ThreadPoolTaskExecutor

5.4 方式一:execute 异步后台导入(立即返回)

Plain 复制代码
@Resource
@Qualifier("asyncBatchExecutor")
private ThreadPoolTaskExecutor batchThreadPool;

@PostMapping("/batchImport")
public String batchImport(@RequestBody List<SysUser> allUserList) {
    int pageSize = 20;
    int total = allUserList.size();

    for (int i = 0; i < total; i += pageSize) {
        int end = Math.min(i + pageSize, total);
        List<SysUser> subList = allUserList.subList(i, end);
        // 核心优化:断开原集合视图关联
        List<SysUser> saveData = new ArrayList<>(subList);

        batchThreadPool.execute(() -> {
            LocalDateTime now = LocalDateTime.now();
            saveData.forEach(u -> {
                u.setCreateTime(now);
                u.setUpdateTime(now);
            });
            sysUserService.saveBatch(saveData);
        });
    }
    return "批量导入任务已提交后台执行!总数据量:" + total;
}

5.5 方式二:submit + Future 等待全部完成再返回

Plain 复制代码
@PostMapping("/batchImportWait")
public String batchImportWait(@RequestBody List<SysUser> allUserList) throws Exception {
    int pageSize = 20;
    int total = allUserList.size();
    List<Future<?>> futureList = new ArrayList<>();

    for (int i = 0; i < total; i += pageSize) {
        int end = Math.min(i + pageSize, total);
        List<SysUser> subList = allUserList.subList(i, end);
        List<SysUser> saveData = new ArrayList<>(subList);

        Future<?> future = batchThreadPool.submit(() -> {
            System.out.printf("【线程:%s】开始分片入库%n", Thread.currentThread().getName());
            LocalDateTime now = LocalDateTime.now();
            saveData.forEach(u -> {
                u.setCreateTime(now);
                u.setUpdateTime(now);
            });
            sysUserService.saveBatch(saveData);
            System.out.printf("【线程:%s】分片入库完成%n", Thread.currentThread().getName());
        });
        futureList.add(future);
    }

    // 阻塞等待所有任务完成
    for (Future<?> future : futureList) {
        future.get();
    }
    return "✅ 所有分片导入全部执行完成!";
}

5.6 进阶优化:CompletableFuture(杜绝轮询、高效任务编排)

传统 Future 存在阻塞等待、无法回调、轮询浪费 CPU 等问题,企业级推荐 CompletableFuture

Plain 复制代码
@PostMapping("/batchImportCompletable")
public String batchImportCompletable(@RequestBody List<SysUser> allUserList) throws Exception {
    int pageSize = 20;
    int total = allUserList.size();
    List<CompletableFuture<Void>> futureList = new ArrayList<>();

    for (int i = 0; i < total; i += pageSize) {
        int end = Math.min(i + pageSize, total);
        List<SysUser> saveData = new ArrayList<>(allUserList.subList(i, end));

        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            LocalDateTime now = LocalDateTime.now();
            saveData.forEach(u -> {
                u.setCreateTime(now);
                u.setUpdateTime(now);
            });
            sysUserService.saveBatch(saveData);
        }, batchThreadPool);

        futureList.add(future);
    }

    // 等待全部任务结束
    CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0])).get();
    return "✅ CompletableFuture 批量导入全部完成";
}

优势:支持回调、异常统一处理、无需空轮询、CPU 利用率高。


六、场景四:线程安全实战(Spring单例 + HashMap 并发深坑)

6.1 核心前置理论

  • Spring Controller 默认单例,全局唯一对象

  • Controller 成员变量 所有请求线程共享

  • HashMap、ArrayList 非线程安全

6.2 错误代码(线上高危BUG)

Plain 复制代码
// 危险!多线程共享非线程安全集合
private Map<Long, SysUser> userCache = new HashMap<>();

@GetMapping("/getWithCache/{id}")
public Result<SysUser> getWithCache(@PathVariable Long id){
    if(userCache.containsKey(id)){
        return Result.success(userCache.get(id));
    }
    SysUser user = sysUserService.getById(id);
    if(user != null){
        userCache.put(id, user);
    }
    return Result.success(user);
}

6.3 为什么 Postman 单 ID 并发测不出问题?

  • 单 ID 并发:只有第一个线程 put,后续全部走缓存读

  • 没有并发写,不会触发 HashMap 扩容死循环、数据丢失问题

  • 多 ID 并发写入 + 触发扩容才会稳定复现线程安全问题

6.4 Python 并发压测脚本(可复现隐患)

Plain 复制代码
import threading
import requests

THREAD_NUM = 50

def request_user(user_id):
    url = f"http://localhost:8080/user/getWithCache/{user_id}"
    try:
        resp = requests.get(url, timeout=5)
        print(f"线程{threading.current_thread().name} | id={user_id} | 响应码:{resp.status_code}")
    except Exception as e:
        print(f"请求异常 id={user_id} 错误:{e}")

if __name__ == '__main__':
    thread_list = []
    for i in range(1, THREAD_NUM + 1):
        t = threading.Thread(target=request_user, args=(i,))
        thread_list.append(t)
        t.start()

    for t in thread_list:
        t.join()
    print("全部请求执行完毕")

6.5 最终正确解决方案

Plain 复制代码
// 线程安全集合,高并发推荐
private Map<Long, SysUser> userCache = new ConcurrentHashMap<>();

6.6 终极线程安全判定口诀

  1. 是否多线程共享同一个对象?

  2. 是否存在并发写操作?

  3. 满足两点 → 必然线程安全风险

方法内局部变量,线程私有,永远安全。


七、四大场景横向对比总结(面试/工作必背)

场景 实现方式 核心特点 适用业务
场景1 @Async void 无返回、不阻塞、后台执行 日志、推送、短信
场景2 @Async + Future 多任务并行、最后汇总结果 数据统计、多接口并行查询
场景3 手动线程池 execute/submit/CompletableFuture 动态分片、大批量任务、高度灵活 批量导入、数据同步
场景4 ConcurrentHashMap 并发安全 解决共享变量线程安全问题 内存缓存、并发容器

八、全文终极开发规范总结

  1. 禁止使用默认线程池,业务必须自定义线程池隔离

  2. @Async 必须独立 Service,杜绝本类调用

  3. 并行任务:先批量提交,再统一获取结果

  4. subList 跨线程必须 new 新集合

  5. 线程池注入类型统一使用 ThreadPoolTaskExecutor

  6. 并发读写共享容器,坚决使用 ConcurrentHashMap

  7. 优先使用 CompletableFuture 做现代异步编排

相关推荐
野生风长1 小时前
c++类和对象(this指针,重载operator,习题总结)
java·开发语言·c++
霸道流氓气质1 小时前
基于 Spring 事务同步机制的事务后置动作收集器 Starter 实践
java·后端·spring
过期动态2 小时前
【LeetCode 热题 100】找到字符串中所有字母异位词
java·数据结构·算法·leetcode·职场和发展·rabbitmq
刘小八2 小时前
Spring AI Tool Calling 生产化:参数校验、权限控制与超时隔离
java·人工智能·spring
奶糖 肥晨2 小时前
一次Spring Boot编译报错排查:三元运算符与包装类型的“隐形陷阱”
java·spring boot·后端
谢栋_2 小时前
设计模式从入门到精通之(七)责任链模式
java·设计模式·责任链模式
愚公移码2 小时前
蓝凌EKP18产品:核心执行流程
java·流程引擎
VortMall2 小时前
全维度打磨细节体验,赋能商城稳定有序运营|VortMall 微服务商城 v1.3.11 版本发布
java·微服务·云原生·架构·商城系统·开源商城·vortmall
Tirzano2 小时前
java 精简使用ffmpeg
java·开发语言·ffmpeg