SpringBoot 业务多线程四大实战场景(全网超详细落地总结)
一、前言
很多同学学习多线程,只会背 Thread、线程池原理、锁机制,但是完全不知道在真实业务中怎么用、什么时候用、用哪种方案。
在真实 CRUD 业务开发中,多线程并不是用来"炫技",而是为了:
-
缩短接口响应时间
-
并行处理多个无依赖任务
-
大批量数据提速处理
-
规避线程安全线上事故
本文基于 SpringBoot + MyBatis-Plus 三层架构 ,从零落地 4 套企业级业务多线程场景,包含:
-
@Async 无返回异步(日志/通知)
-
@Async + Future 并行查询汇总统计
-
手动自定义线程池 + 分片批量导入(execute / submit / CompletableFuture)
-
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 业务场景
用户数据统计页面,需要同时查询:
-
正常用户列表
-
禁用用户列表
-
用户总数
三个任务 无依赖关系,可以并行执行,大幅缩短接口耗时。
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 核心两大提交方式
-
execute():无返回、不等待、后台执行 -
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 | @Async void | 无返回、不阻塞、后台执行 | 日志、推送、短信 |
| 场景2 | @Async + Future | 多任务并行、最后汇总结果 | 数据统计、多接口并行查询 |
| 场景3 | 手动线程池 execute/submit/CompletableFuture | 动态分片、大批量任务、高度灵活 | 批量导入、数据同步 |
| 场景4 | ConcurrentHashMap 并发安全 | 解决共享变量线程安全问题 | 内存缓存、并发容器 |
八、全文终极开发规范总结
-
禁止使用默认线程池,业务必须自定义线程池隔离
-
@Async 必须独立 Service,杜绝本类调用
-
并行任务:先批量提交,再统一获取结果
-
subList 跨线程必须 new 新集合
-
线程池注入类型统一使用 ThreadPoolTaskExecutor
-
并发读写共享容器,坚决使用 ConcurrentHashMap
-
优先使用 CompletableFuture 做现代异步编排