一、背景
如果你是一位面试经验丰富的求职者,你会发现线程池相关面试题出现的概率在80%以上,面试题目无非下面几个:1)工作中有没有使用过线程池啊?怎么使用的?2)说下线程池的参数?3)说下线程池的工作原理;4)说下线程池的拒绝策略有哪些?5)说下线程池的线程数是如何确认的,如何优化?我相信以上问题,通过八股文基本上都可以搞定。
但是我一直有一个疑问,线程池的工作原理你既然有理论知识,可以用代码示例来给我演示一下吗?本文主要通过代码demo打印日志来演示线程池怎么工作的。
二、Spring默认线程池是什么?
Spring默认线程池是simpleAsyncTaskExecutor,解释如下:
默认情况下,Spring将搜索关联的线程池定义:Spring上下文容器中的唯一的org.springframework.core.task.TaskExecutor类型的bean,如果不存在,则查找名为“taskExecutor”的java.util.concurrent.Executo的 bean。如果两者都不存在,则将使用org.springframework.core.task.SimpleAsyncTaskExecutor的一个实例来处理异步方法调用。
SimpleAsyncTaskExecutor中对每个异步任务对应开启一个线程来进行处理,会造成线程频繁创建与销毁,没有进行线程复用,所以我们可以创建自己的线程池。
代码示例:
@SpringBootApplication
@EnableAsync
@EnableScheduling
public class GoogleApplication {
public static void main(String[] args) {
SpringApplication.run(GoogleApplication.class, args);
}
}
@Component
@EnableAsync
public class ScheduleTask {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Async
@Scheduled(fixedRate = 1000)
public void testScheduleTask() {
try {
System.out.println("Spring-1开始执行:" + Thread.currentThread().getName() + "-" + sdf.format(new Date()));
Thread.sleep(1000);
System.out.println("Spring-1结束执行" + Thread.currentThread().getName() + "-" + sdf.format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Async
@Scheduled(cron = "*/2 * * * * ?")
public void testAsyn() {
try {
System.out.println("Spring-2开始执行:" + Thread.currentThread().getName() + "-" + sdf.format(new Date()));
Thread.sleep(2000);
System.out.println("Spring-2结束执行:" + Thread.currentThread().getName() + "-" + sdf.format(new Date()));
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
打印日志:
Spring-1开始执行:SimpleAsyncTaskExecutor-1-2022-03-23 15:38:01
Spring-2开始执行:SimpleAsyncTaskExecutor-2-2022-03-23 15:38:02
Spring-1结束执行SimpleAsyncTaskExecutor-1-2022-03-23 15:38:02
Spring-1开始执行:SimpleAsyncTaskExecutor-3-2022-03-23 15:38:02
Spring-1开始执行:SimpleAsyncTaskExecutor-4-2022-03-23 15:38:03
Spring-1结束执行SimpleAsyncTaskExecutor-3-2022-03-23 15:38:03
Spring-2开始执行:SimpleAsyncTaskExecutor-5-2022-03-23 15:38:04
Spring-2结束执行:SimpleAsyncTaskExecutor-2-2022-03-23 15:38:04
Spring-1开始执行:SimpleAsyncTaskExecutor-6-2022-03-23 15:38:04
Spring-1结束执行SimpleAsyncTaskExecutor-4-2022-03-23 15:38:04
Spring-1结束执行SimpleAsyncTaskExecutor-6-2022-03-23 15:38:05
Spring-1开始执行:SimpleAsyncTaskExecutor-7-2022-03-23 15:38:05
Spring-2开始执行:SimpleAsyncTaskExecutor-8-2022-03-23 15:38:06
Spring-2结束执行:SimpleAsyncTaskExecutor-5-2022-03-23 15:38:06
从日志信息我们可以得到两个信息;
1)Spring默认线程池是SimpleAsyncTaskExecutor
2)每一个异步任务需要开启一个线程来进行,看线程编码可知
3)通过时间对比发现,testScheduleTask() 每1s发起一个任务,testAsyn()每2s发起一个任务,互相无关系,单独执行。
三、自定义线程池
@Configuration
public class AsyncScheduledTaskConfig {
@Value("${spring.task.execution.pool.core-size}")
private int corePoolSize;
@Value("${spring.task.execution.pool.max-size}")
private int maxPoolSize;
@Value("${spring.task.execution.pool.queue-capacity}")
private int queueCapacity;
@Value("${spring.task.execution.thread-name-prefix}")
private String namePrefix;
@Value("${spring.task.execution.pool.keep-alive}")
private int keepAliveSeconds;
@Bean
public Executor myAsync() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 最大线程数
executor.setMaxPoolSize(maxPoolSize);
// 核心线程数
executor.setCorePoolSize(corePoolSize);
// 任务队列的大小
executor.setQueueCapacity(queueCapacity);
// 线程前缀名
executor.setThreadNamePrefix(namePrefix);
// 线程存活时间
executor.setKeepAliveSeconds(keepAliveSeconds);
// 拒绝策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
// 线程初始化
executor.initialize();
return executor;
}
}
配置文件:
# 核心线程池数
spring.task.execution.pool.core-size=4
# 最大线程池数
spring.task.execution.pool.max-size=8
# 任务队列的容量
spring.task.execution.pool.queue-capacity=4
# 非核心线程的存活时间
spring.task.execution.pool.keep-alive=60
# 线程池的前缀名称
spring.task.execution.thread-name-prefix=Snow-river-task-
ScheduleTask.class 文件中的testScheduleTask()方法增加参数@Async("myAsync")
@Async("myAsync")
@Scheduled(fixedRate = 1000)
public void testScheduleTask() {
try {
System.out.println("Spring1进入" + Thread.currentThread().getName() + "-" + sdf.format(new Date()));
Thread.sleep(5950);
System.out.println("Spring1自带的线程池" + Thread.currentThread().getName() + "-" + sdf.format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
通过以上配置文件编写就完成了一个核心线程数为4,最大线程数为8,任务队列为4的线程池(用于第五步解释)
四、线程池处理流程
1)当一个新任务来的时候,先使用核心线程来进行执行;
2)当核心线程数满了的时候,将新的任务加到任务队列,等待执行;
3)当任务队列也满的时候,则新开启一个线程用于任务的执行;
4)当最大线程数也满了(线程池都满了),任务队列也满了的时候,这时候就需要执行拒绝策略了。
流程图(Viso):
五、通过日志分析线程池工作原理
1)我们只使用testScheduleTask()这一个方法,将testAsyn()方法注释掉。将定时任务时间设置为1s,sleep时间设置为5950ms(因为程序运行需要时间,设置6s的话,效果不明显)
Spring1开始:Snow-river-task-1-2022-03-23 16:18:16
Spring1开始:Snow-river-task-2-2022-03-23 16:18:17
Spring1开始:Snow-river-task-3-2022-03-23 16:18:18
Spring1开始:Snow-river-task-4-2022-03-23 16:18:19
Spring1结束任务执行:Snow-river-task-1-2022-03-23 16:18:22
Spring1开始:Snow-river-task-1-2022-03-23 16:18:22
Spring1结束任务执行:Snow-river-task-2-2022-03-23 16:18:23
Spring1开始:Snow-river-task-2-2022-03-23 16:18:23
Spring1结束任务执行:Snow-river-task-3-2022-03-23 16:18:24
Spring1开始:Snow-river-task-3-2022-03-23 16:18:24
Spring1结束任务执行:Snow-river-task-4-2022-03-23 16:18:25
Spring1开始:Snow-river-task-4-2022-03-23 16:18:25
Spring1结束任务执行:Snow-river-task-1-2022-03-23 16:18:28
Spring1开始:Snow-river-task-1-2022-03-23 16:18:28
Spring1结束任务执行:Snow-river-task-2-2022-03-23 16:18:29
Spring1开始:Snow-river-task-2-2022-03-23 16:18:29
Spring1结束任务执行:Snow-river-task-3-2022-03-23 16:18:30
Spring1开始:Snow-river-task-3-2022-03-23 16:18:30
Spring1结束任务执行:Snow-river-task-4-2022-03-23 16:18:31
Spring1开始:Snow-river-task-4-2022-03-23 16:18:31
Spring1开始:Snow-river-task-5-2022-03-23 16:18:32
Spring1开始:Snow-river-task-6-2022-03-23 16:18:33
Spring1结束任务执行:Snow-river-task-1-2022-03-23 16:18:34
Spring1开始:Snow-river-task-1-2022-03-23 16:18:34
Spring1结束任务执行:Snow-river-task-2-2022-03-23 16:18:35
Spring1开始:Snow-river-task-2-2022-03-23 16:18:35
Spring1结束任务执行:Snow-river-task-3-2022-03-23 16:18:36
Spring1开始:Snow-river-task-3-2022-03-23 16:18:36
Spring1结束任务执行:Snow-river-task-4-2022-03-23 16:18:37
Spring1开始:Snow-river-task-4-2022-03-23 16:18:37
Spring1结束任务执行:Snow-river-task-5-2022-03-23 16:18:38
Spring1开始:Snow-river-task-5-2022-03-23 16:18:38
Spring1结束任务执行:Snow-river-task-6-2022-03-23 16:18:39
Spring1开始:Snow-river-task-6-2022-03-23 16:18:39
分析:task任务是每秒产生一个,因此我们可以用秒数作为task的编号,因为存在系统运行时间,我们假设代码阻塞时间为6s(sleep(5950),6s需要减去System.out时间,假设为50ms)。结束时间也就是线程的释放时间。
1)开始排队的时间点发生在20s,目前只有2个队列
2)从28s开始执行的任务,队列的阻塞数量已经达到了4个(满了)
3)在32s的时候,有新任务产生,但是阻塞队列已经满了,现在还没有任务释放(task-1)释放是在34s的时候,所以此时此刻,不得不新开一个线程去执行该任务。
如果将sleep时间设置为8050ms,则会执行拒绝策略,因为线程池用完了,新的任务没有线程池可用。这个可以自己分析下。
Spring1开始:Snow-river-task-6-2022-03-23 16:10:43
Spring1开始:Snow-river-task-7-2022-03-23 16:10:44
Spring1开始:Snow-river-task-8-2022-03-23 16:10:45
2022-03-23 16:10:46.617 ERROR 17028 --- [ scheduling-1] o.s.s.s.TaskUtils$LoggingErrorHandler : Unexpected error occurred in scheduled task.
org.springframework.core.task.TaskRejectedException: Executor [java.util.concurrent.ThreadPoolExecutor@6939dccb[Running, pool size = 8, active threads = 8, queued tasks = 4, completed tasks = 4]] did not accept task: org.springframework.aop.interceptor.AsyncExecutionInterceptor$$Lambda$579/305419323@58baf6b
at org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor.submit(ThreadPoolTaskExecutor.java:344) ~[spring-context-5.1.9.RELEASE.jar:5.1.9.RELEASE]
六、参考文献
1、spring async 默认线程池_Spring框架异步执行
https://blog.csdn.net/weixin_39760857/article/details/111391555
2、Spring自带的线程池ThreadPoolTaskExecutor
https://zhuanlan.zhihu.com/p/346086161