问java线程池解决上面问题,常见使用场景是什么
这就触及到了线程池最核心的"自我保护"机制!当线程池里的所有线程(包括核心线程和临时线程)都在忙,并且排队的任务也塞满时,线程池就会启动最后一道防线------拒绝策略。
简单来说,当"并发量太大,实在处理不过来"时,Java 线程池提供了 4 种标准的应对方式(也就是 4 种拒绝策略),你可以根据业务的重要程度来选择:
🛑 1. 直接报错 (AbortPolicy) ------ 这是默认策略
- 处理方式:直接抛出一个异常,告诉提交任务的人:"别发了,我这边已经爆了!"
- 适用场景 :适用于那些绝对不能丢、也不能拖的关键业务。比如支付扣款,如果系统处理不过来,必须立刻让前端知道失败了,以便提示用户重试,而不是默默把任务丢掉。
🏃 2. 让提交者自己干 (CallerRunsPolicy) ------ 最推荐的"削峰"策略
- 处理方式 :线程池虽然忙,但它不拒绝任务。它会把新来的任务"退回去",让提交这个任务的主线程自己去执行这个任务。
- 适用场景 :这是一种非常聪明的**"反压(Backpressure)"机制**。比如你的 Web 服务器(Tomcat)线程把任务丢给线程池,线程池满了,Tomcat 线程只能自己去干活。这样一来,Tomcat 线程忙于执行任务,就没空接收新的 HTTP 请求了,相当于自动给系统降速,防止洪峰流量把服务器彻底冲垮。
🗑️ 3. 悄悄丢弃任务 (DiscardPolicy)
- 处理方式:新来的任务直接被扔掉,不抛异常,也不做任何处理,就像没发生过一样。
- 适用场景 :适用于完全不重要、丢了也无所谓的业务。比如你在后台记录一些非核心的用户行为日志、点击流数据等,高并发时丢掉一部分对整体业务没有任何影响。
🔄 4. 丢弃最老的任务 (DiscardOldestPolicy)
- 处理方式 :把任务队列里排队时间最长(最旧)的那个任务踢掉,然后尝试把当前这个新任务塞进队列里。
- 适用场景:适用于**"喜新厌旧"**的业务。比如实时刷新股票行情、最新的系统监控数据等,我们只关心最新的状态,旧的数据早就过时了,丢了反而更好。
📌 结合你的学生管理系统来理解
假设你的系统里有一个"批量给学生发送邮件"的功能,使用了线程池来处理:
- 如果用默认策略(直接报错):期末考试出分,几千个家长同时查分触发邮件发送。线程池满了之后,后面的查分请求直接报错,家长看到页面提示"系统异常"。
- 如果用 CallerRunsPolicy(让提交者自己干) :线程池满了,Web 服务器被迫自己去发邮件。虽然发得慢,网页加载变卡了,但没有任何一个家长的查分请求会失败,系统扛过了高峰期。
- 如果用 DiscardPolicy(悄悄丢弃):线程池满了,后面几百个家长的邮件直接没发出去。家长查分成功了,但死活收不到邮件,开始疯狂投诉(这种场景绝对不能用丢弃策略)。
💡 进阶提示:
在实际大厂开发中,除了这 4 种内置策略,我们经常会自定义拒绝策略 。比如当线程池满了,不直接丢弃,而是把任务写入到数据库或者消息队列(如 Kafka、RabbitMQ)里,等系统空闲了,再通过定时任务把这些积压的任务重新捞出来处理,从而实现数据的"最终一致性"。
问那比如分了多个线程还不够,那其余并发怎么处理
这就触及到了线程池最核心的"自我保护"机制!当线程池里的所有线程(包括核心线程和临时线程)都在忙,并且排队的任务也塞满时,线程池就会启动最后一道防线------拒绝策略。
简单来说,当"并发量太大,实在处理不过来"时,Java 线程池提供了 4 种标准的应对方式(也就是 4 种拒绝策略),你可以根据业务的重要程度来选择:
🛑 1. 直接报错 (AbortPolicy) ------ 这是默认策略
- 处理方式:直接抛出一个异常,告诉提交任务的人:"别发了,我这边已经爆了!"
- 适用场景 :适用于那些绝对不能丢、也不能拖的关键业务。比如支付扣款,如果系统处理不过来,必须立刻让前端知道失败了,以便提示用户重试,而不是默默把任务丢掉。
🏃 2. 让提交者自己干 (CallerRunsPolicy) ------ 最推荐的"削峰"策略
- 处理方式 :线程池虽然忙,但它不拒绝任务。它会把新来的任务"退回去",让提交这个任务的主线程自己去执行这个任务。
- 适用场景 :这是一种非常聪明的**"反压(Backpressure)"机制**。比如你的 Web 服务器(Tomcat)线程把任务丢给线程池,线程池满了,Tomcat 线程只能自己去干活。这样一来,Tomcat 线程忙于执行任务,就没空接收新的 HTTP 请求了,相当于自动给系统降速,防止洪峰流量把服务器彻底冲垮。
🗑️ 3. 悄悄丢弃任务 (DiscardPolicy)
- 处理方式:新来的任务直接被扔掉,不抛异常,也不做任何处理,就像没发生过一样。
- 适用场景 :适用于完全不重要、丢了也无所谓的业务。比如你在后台记录一些非核心的用户行为日志、点击流数据等,高并发时丢掉一部分对整体业务没有任何影响。
🔄 4. 丢弃最老的任务 (DiscardOldestPolicy)
- 处理方式 :把任务队列里排队时间最长(最旧)的那个任务踢掉,然后尝试把当前这个新任务塞进队列里。
- 适用场景:适用于**"喜新厌旧"**的业务。比如实时刷新股票行情、最新的系统监控数据等,我们只关心最新的状态,旧的数据早就过时了,丢了反而更好。
📌 结合你的学生管理系统来理解
假设你的系统里有一个"批量给学生发送邮件"的功能,使用了线程池来处理:
- 如果用默认策略(直接报错):期末考试出分,几千个家长同时查分触发邮件发送。线程池满了之后,后面的查分请求直接报错,家长看到页面提示"系统异常"。
- 如果用 CallerRunsPolicy(让提交者自己干) :线程池满了,Web 服务器被迫自己去发邮件。虽然发得慢,网页加载变卡了,但没有任何一个家长的查分请求会失败,系统扛过了高峰期。
- 如果用 DiscardPolicy(悄悄丢弃):线程池满了,后面几百个家长的邮件直接没发出去。家长查分成功了,但死活收不到邮件,开始疯狂投诉(这种场景绝对不能用丢弃策略)。
💡 进阶提示:
在实际大厂开发中,除了这 4 种内置策略,我们经常会自定义拒绝策略 。比如当线程池满了,不直接丢弃,而是把任务写入到数据库或者消息队列(如 Kafka、RabbitMQ)里,等系统空闲了,再通过定时任务把这些积压的任务重新捞出来处理,从而实现数据的"最终一致性"。
问那怎么引入线程池。具体代码怎么写,什么步骤,技术栈还是spring-boot + mybatis-plus
在 Spring Boot + MyBatis-Plus 技术栈中引入线程池,最优雅且标准的方式是结合 Spring 的 @Async 注解来实现。
整个过程分为 4 个核心步骤,我为你整理了最简洁、最不容易踩坑的代码写法:
🛠️ 步骤一:创建线程池配置类(核心)
在项目中创建一个配置类,用来定义你的线程池。切记:千万不要直接裸用 @Async,必须自定义线程池,否则高并发下会无限创建线程导致服务器崩溃。
java
package com.example.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
@EnableAsync // 1. 开启异步任务支持
public class ThreadPoolConfig {
@Bean("myTaskExecutor") // 2. 给线程池起个名字,方便后面指定使用
public Executor myTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数(常驻干活的线程)
executor.setCorePoolSize(5);
// 最大线程数(高峰期最多招多少人)
executor.setMaxPoolSize(10);
// 任务队列容量(候客区能坐多少人)
executor.setQueueCapacity(100);
// 线程空闲存活时间(秒)
executor.setKeepAliveSeconds(60);
// 线程名字前缀(方便排查日志时知道是哪个线程)
executor.setThreadNamePrefix("MyAsync-");
// 拒绝策略:队列和线程都满了,让提交任务的主线程自己去执行(削峰保命)
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
⚙️ 步骤二:在启动类上开启异步支持
在你的 Spring Boot 主启动类上加上 @EnableAsync 注解(如果你已经在配置类上加了,这里可以不加,但加上是个好习惯)。
java
@SpringBootApplication
@EnableAsync
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
💼 步骤三:在 Service 层编写异步业务方法
把那些耗时的、需要异步执行的任务(比如批量插入数据、发邮件等)单独写在一个方法里,并打上 @Async 标签,指定使用刚才配置的线程池。
java
package com.example.demo.service;
import com.example.demo.entity.Student;
import com.example.demo.mapper.StudentMapper;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
@Service
public class StudentService {
@Resource
private StudentMapper studentMapper;
// 指定使用名为 "myTaskExecutor" 的线程池来执行这个方法
@Async("myTaskExecutor")
public void batchInsertStudents(List<Student> studentList) {
// 这里写耗时的数据库操作,MyBatis-Plus 的批量插入
for (Student student : studentList) {
studentMapper.insert(student);
}
System.out.println("线程[" + Thread.currentThread().getName() + "] 批量插入完成!");
}
}
🚀 步骤四:在 Controller 或其他地方调用
直接像调用普通方法一样去调用它即可。Spring 会自动把这个任务丢给线程池去后台执行,主线程会立刻继续往下走,不会被卡住。
java
package com.example.demo.controller;
import com.example.demo.entity.Student;
import com.example.demo.service.StudentService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
@RestController
public class StudentController {
@Resource
private StudentService studentService;
@PostMapping("/import-students")
public String importStudents() {
// 模拟一堆学生数据
List<Student> students = new ArrayList<>();
for (int i = 0; i < 100; i++) {
Student s = new Student();
s.setName("学生" + i);
students.add(s);
}
// 调用异步方法(主线程发完任务立刻返回,不等它插完数据库)
studentService.batchInsertStudents(students);
return "数据导入任务已提交到后台处理,请稍后查看!";
}
}
⚠️ 新手避坑指南(非常重要)
- 同类调用失效 :
@Async注解是基于 Spring AOP(动态代理)实现的。不能在同一个类里面直接调用加了@Async的方法,否则注解会失效,变成同步执行。一定要在 Controller 里注入 Service 来调用,或者通过注入自身 Bean 的方式调用。 - 数据库连接池大小 :使用了线程池后,并发量会变大。记得去
application.yml里检查一下你的数据库连接池(如 HikariCP)的maximum-pool-size。如果线程池最大有 10 个线程在干活,但数据库连接池只有 5 个连接,线程就会因为抢不到数据库连接而阻塞。 - 方法必须是 public :加了
@Async的方法必须是public修饰的,否则无法被代理。
按照这四步走,你就成功在 Spring Boot 项目中引入了一个安全、可控的线程池!
问这个线程池,是对所有项目所有接口起作用是吗?
不是的 。我们刚才配置的这个线程池,只对明确使用了 @Async 注解的方法起作用,绝对不会影响到你项目里所有的接口和请求。
为了让你更清晰地理解,我们可以从以下三个维度来看:
1. 它只管"被标记"的异步任务
这个线程池就像一个**"特种部队"**。
- 你项目里绝大多数的普通接口(比如简单的增删改查、页面跳转),依然走的是 Spring Boot 默认的 Web 服务器(比如 Tomcat)的普通线程,完全不会经过这个线程池。
- 只有当你给某个方法打上
@Async标签,并且指定了@Async("myTaskExecutor")时,这个方法才会被丢进这个"特种部队"的营地里去执行。
2. 不同的线程池,分工不同
在一个标准的 Spring Boot 项目中,其实同时运行着好几套线程池,它们各司其职:
- Tomcat 线程池(默认):负责接待所有的 HTTP 请求(比如用户打开网页、点击按钮)。这是项目的主线程池,我们自定义的线程池不会干扰它。
- 自定义的
myTaskExecutor(我们刚写的):专门用来处理你指定的耗时后台任务(比如发邮件、批量导数据)。 - 数据库连接池(HikariCP):专门负责和数据库打交道。
所以,你自定义的线程池只是项目里的一个"小部门",专门处理脏活累活,不会影响"大部队"(普通接口)的正常运转。
3. 甚至可以配置多个线程池
如果以后你的项目变大了,你还可以创建多个线程池来分别处理不同的业务。
比如:
- 创建一个
emailExecutor专门发邮件。 - 创建一个
reportExecutor专门生成报表。
你只需要在配置类里多写几个 @Bean,然后在不同的 Service 方法上通过 @Async("emailExecutor") 或 @Async("reportExecutor") 来指定即可。它们之间互不干扰,非常灵活。
总结一下:
你刚才写的线程池配置,只对那些你主动加了 @Async("myTaskExecutor") 的方法生效。其他的普通接口依然按照原来的方式运行,完全不受影响,你可以放一百个心!