一.什么是线程池
线程池其实就是一种多线程处理形式,处理过程中可以将任务添加到队列中,然后在创建线程后自动启动这些任务。这里的线程就是我们前面学过的线程,这里的任务就是我们前面学过的实现了Runnable或Callable接口的实例对象。
为什么使用多线程
1.线程管理复杂 何时创建线程,何时销毁线程
2.任务管理复杂 任务什么时候接收,任务什么时候拒绝
二.线程池的实现及原理
- Spring 中可以用 ThreadPoolTaskExecutor 配合 @Async 注解来实现(不太推荐)
- Java 中,可以使用 JUC 并发包中的 ThreadPoolExecutor 来实现非常灵活的自定义线程池(推荐)。
线程池参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
- corePoolSize(核心线程数 => 正式员工数量):正常情况下,我们系统应该能同时工作的线程数
- maximumPoolSize(最大线程数 =>最多招多少人):极限情况下,我们线程池最多有多少个线程。
- keepAliveTime(空闲线程存活时间):非核心线程在没有任务的情况下,过多久删除掉(理解为开除临时工)
- TimeUnit unit(空闲线程存活时间的单位):分钟、秒
- workQueue(工作队列):用于存放给线程执行的任务,存在队列的最大长度(一定要设置队列长度,不要设置成无限大)。
- threadFactory(线程工厂):控制每个线程的生成、线程的属性(比如线程名)
- RejectedExecutionHandler(拒绝策略):任务队列满的时候,我们采取什么策略,比如抛异常、不抛异常,自定义策略。
工作流程图
线程池工作机制
- 核心线程:
- 初始时,线程池有
corePoolSize
个核心线程(例如2个)。 - 当提交新任务时,如果核心线程数未满,则直接由核心线程处理。
- 初始时,线程池有
- 任务队列:
- 当核心线程都在忙碌时,新任务会被放入
workQueue
(阻塞队列)中等待处理。 workQueue
的大小限制了能够等待的任务数量。
- 当核心线程都在忙碌时,新任务会被放入
- 最大线程数:
- 如果
workQueue
已满,且当前线程数小于maximumPoolSize
(例如4个),则线程池会创建新的临时线程来处理任务。
- 如果
- 拒绝策略:
- 当
workQueue
已满,且线程数已达到maximumPoolSize
时,再提交新任务会触发RejectedExecutionHandler
定义的拒绝策略(如抛异常、丢弃任务等)。
- 当
- 空闲线程超时:
- 当线程池中的线程数量超过
corePoolSize
时,如果某个线程在keepAliveTime
时间段内没有执行任务,那么该线程会被终止,以减少资源消耗。
- 当线程池中的线程数量超过
举例有9个任务的情况
核心线程数为2 最大线程数为4 任务队列长度为4
核心线程是正式工,非核心线程是临时工
加入任务1,2 分配给核心线程1,2
加入任务3,此时核心线程数已满,看任务队列是否空闲,加入队列
陆续加入4,5,6到队列中
此时队列已满,任务7需要创建线程a,处理
任务8创建线程b处理
当有任务9时,此时最大线程数已满,无法创建线程,拒绝执行
如何确定线程池参数?
根据实际情况(实际业务场景 和 系统资源)来调整测试,不断优化参数。
比如:现有条件 AI 并发只允许 4 个任务同时执行,允许 20 个任务排队。
以上需求可设置如下参数:
- corePoolSize(核心线程数):正常情况设置为 4
- maximumPoolSize(最大线程数):设置为极限情况,数量 >= 4
- keepAliveTime(空闲线程存活时间):一般设置为秒级/分钟级
- TimeUnit unit(空闲线程存活时间的单位):分钟、秒
- workQueue(工作队列):结合实际情况,可以设置为 20( AI 允许 20 个任务排队)
- RejectedExecutionHandler(拒绝策略):抛异常,标记数据库任务状态为 “任务满了,已拒绝”
任务的划分(I/O 或计算密集型)
CPU (计算)密集型:计算较多,比如音视频处理、图像处理、数学计算等,一般设置 corePoolSize 为 CPU 的核心数 + 1 ( N+1 )。
I/O 密集型:吃带宽/内存/硬盘的读写资源,corePoolSize 可以设置大一点,通常经验值为 2N(N 为核心数),但建议以 I/O 能力为主。
优点:合理设置核心数能够让线程最大化利用 CPU,不至于频繁切换线程。
三.线程池示例代码
@Configuration
//创建线程池配置类
public class ThreadPoolExecutorConfig {
@Bean //方法返回值对象加入容器里面
public ThreadPoolExecutor generate(){
//新建一个线程工厂
ThreadFactory threadFactory=new ThreadFactory() {
int count=1;//初始线程数
@Override//重写线程创建方法
//@NotNull保证参数r不为空
public Thread newThread(@NotNull Runnable r) {
Thread thread = new Thread(r);
thread.setName("线程"+count);//设置线程名字为线程数
count++;
return thread;//返回线程
}
};//创建一个线程池对象 指定各个参数//任务队列是阻塞队列//自定义线程工厂创建线程
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 4, 100, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(4), threadFactory);
return threadPoolExecutor;//返回线程池对象
}
}
@Slf4j
@RestController
@Controller
@RequestMapping("/queue")
public class QueueController {
@Autowired
ThreadPoolExecutor threadPoolExecutor;
@GetMapping("/add")
public void add(String name){
//CompletableFuture.runAsync()方法是用来异步地执行一个无返回值的Runnable任务
CompletableFuture.runAsync(()->{
log.info("任务执行中"+name+"执行者"+Thread.currentThread().getName());
try {
Thread.sleep(600000);//模拟长时间占用
} catch (InterruptedException e) {
e.printStackTrace();
}
//异步任务在threadPoolExecutor里面执行
},threadPoolExecutor);
}
@PostMapping("/get")
public String get(){
HashMap<String, Object> map = new HashMap<>();
//获取任务总数
long tasktotal=threadPoolExecutor.getTaskCount();
//获取已经完成任务数
long completedTaskCount = threadPoolExecutor.getCompletedTaskCount();
//获取存活时间
int activeCount = threadPoolExecutor.getActiveCount();
//获取核心线程数
int corePoolSize = threadPoolExecutor.getCorePoolSize();
//获取最大线程数
int maximumPoolSize = threadPoolExecutor.getMaximumPoolSize();
//获取队列长度
int size = threadPoolExecutor.getQueue().size();
map.put("任务总数",tasktotal);
map.put("已经完成任务数",completedTaskCount);
map.put("存活时间",activeCount);
map.put("核心线程数",corePoolSize);
map.put("最大线程数",maximumPoolSize);
map.put("队列长度",size);
return JSONUtil.toJsonStr(map);
}
}