并发编程(二):用demo演示线程池工作原理(实战篇)

一、背景

如果你是一位面试经验丰富的求职者,你会发现线程池相关面试题出现的概率在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 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值