- 一、什么是线程池
- 二、线程池的构造
- 三、线程池的运行原理
- 四、线程池中线程实现复用的原理
- 五、线程是如何获取任务的以及如何实现超时的
- 六、线程池的5种状态
- 七、线程池的关闭
- 八、线程池的监控
- 九、Executors构建线程池以及问题分析
- 十、线程池的使用场景
- 十一、实际项目中如何合理的自定义线程池
今天跟大家聊一聊无论是在工作中常用还是在面试中常问的线程池,通过画图的方式来彻底弄懂线程池的工作原理,以及在实际项目中该如何自定义适合业务的线程池。
一、什么是线程池
线程池其实是一种池化的技术的实现,池化技术的核心思想其实就是实现资源的一个复用,避免资源的重复创建和销毁带来的性能开销。在线程池中,线程池可以管理一堆线程,让线程执行完任务之后不会进行销毁,而是继续去处理其它线程已经提交的任务。
线程池的好处:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统 的稳定性,使用线程池可以进行统一的分配,调优和监控。
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
二、线程池的构造
Java中主要是通过构建ThreadPoolExecutor来创建线程池的,接下来我们看一下线程池是如何构造出来的。
线程池构造参数
- corePoolSize:线程池中用来工作的核心的线程数量。
- maximumPoolSize:最大线程数,线程池允许创建的最大线程数。
- keepAliveTime:超出 corePoolSize 后创建的线程存活时间或者是所有线程最大存活时间,取决于配置。
- unit:keepAliveTime 的时间单位。
- workQueue:任务队列,是一个阻塞队列,当线程数已达到核心线程数,会将任务存储在阻塞队列中。
- threadFactory :线程池内部创建线程所用的工厂。
- handler:拒绝策略;当队列已满并且线程数量达到最大线程数量时,会调用该方法处理该任务。
线程池的构造其实很简单,就是传入一堆参数,然后进行简单的赋值操作。
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
三、线程池的运行原理
说完线程池的核心构造参数的意思,接下来就来画图讲解这些参数在线程池中是如何工作的。
线程池刚创建出来是什么样子呢,如下图
不错,刚创建出来的线程池中只有一个构造时传入的阻塞队列而已,此时里面并没有的任何线程,但是如果你想要在执行之前已经创建好核心线程数,可以调用prestartAllCoreThreads方法来实现,默认是没有线程的。
当有线程通过execute方法提交了一个任务,会发生什么呢?
提交任务的时候,其实会去进行任务的处理
首先会去判断当前线程池的线程数是否小于核心线程数,也就是线程池构造时传入的参数corePoolSize。
如果小于,那么就直接通过ThreadFactory创建一个线程来执行这个任务,如图
当任务执行完之后,线程不会退出,而是会去从阻塞队列中获取任务,如下图
接下来如果又提交了一个任务,也会按照上述的步骤,去判断是否小于核心线程数,如果小于,还是会创建线程来执行任务,执行完之后也会从阻塞队列中获取任务。这里有个细节,就是提交任务的时候,就算有线程池里的线程从阻塞队列中获取不到任务,如果线程池里的线程数还是小于核心线程数,那么依然会继续创建线程,而不是复用已有的线程。
如果线程池里的线程数不再小于核心线程数呢?那么此时就会尝试将任务放入阻塞队列中,入队成功之后,如图
这样在阻塞的线程就可以获取到任务了。
但是,随着任务越来越多,队列已经满了,任务放入失败了,那怎么办呢?
此时就会判断当前线程池里的线程数是否小于最大线程数,也就是入参时的maximumPoolSize参数
如果小于最大线程数,那么也会创建非核心线程来执行提交的任务,如图
所以,从这里可以发现,就算队列中有任务,新创建的线程还是优先处理这个提交的任务,而不是从队列中获取已有的任务执行,从这可以看出,先提交的任务不一定先执行。
但是不幸的事发生了,线程数已经达到了最大线程数量,那么此时会怎么办呢?
此时就会执行拒绝策略,也就是构造线程池的时候,传入的RejectedExecutionHandler对象,来处理这个任务。
RejectedExecutionHandler的实现JDK自带的默认有4种
- AbortPolicy:丢弃任务,抛出运行时异常
- CallerRunsPolicy:由提交任务的线程来执行任务
- DiscardPolicy:丢弃这个任务,但是不抛异常
- DiscardOldestPolicy:从队列中剔除最先进入队列的任务,然后再次提交任务
线程池创建的时候,如果不指定拒绝策略就默认是AbortPolicy策略。当然,你也可以自己实现RejectedExecutionHandler接口,比如将任务存在数据库或者缓存中,这样就数据库或者缓存中获取到被拒绝掉的任务了。
到这里,我们发现,线程池构造的几个参数corePoolSize、maximumPoolSize、workQueue、threadFactory、handler我们都在上述的执行过程中讲到了,那么还差两个参数keepAliveTime和unit(unit是keepAliveTime的时间单位)没讲到,所以keepAliveTime是如何起到作用的呢,这个问题留到后面分析。
说完整个执行的流程,接下来看看execute方法代码是如何实现的。
execute方法
- workerCountOf(c)<corePoolSize:这行代码就是判断是否小于核心线程数,是的话就通过addWorker方法,addWorker就是添加线程来执行任务。
- workQueue.offer(command):这行代码就表示尝试往阻塞队列中添加任务
- 添加失败之后就会再次调用addWorker方法尝试添加非核心线程来执行任务
- 如果还是添加非核心线程失败了,那么就会调用reject(command)来拒绝这个任务。
最后再来另画一张图总结execute执行流程
四、线程池中线程实现复用的原理
线程池的核心功能就是实现了线程的重复利用,那么线程池是如何实现线程的复用呢?
线程在线程池内部其实是被封装成一个Worker对象
Worker继承了AQS,也就是有一定锁的特性。
创建线程来执行任务的方法上面提到是通过addWorker方法创建的。在创建Worker对象的时候,会把线程和任务一起封装到Worker内部,然后调用runWorker方法来让线程执行任务,接下来我们就来看一下runWorker方法。
启动线程处理任务
从这张图可以看出线程执行完任务不会退出的原因,runWorker内部使用了while死循环,当第一个任务执行完之后,会不断地通过getTask方法获取任务,只要能获取到任务,就会调用run方法,继续执行任务,这就是线程能够复用的主要原因。
但是如果从getTask获取不到方法的时候,最后就会调用finally中的processWorkerExit方法,来将线程退出。
这里有个一个细节就是,因为Worker继承了AQS,每次在执行任务之前都会调用Worker的lock方法,执行完任务之后,会调用unlock方法,这样做的目的就可以通过Woker的加锁状态就能判断出当前线程是否正在运行任务。如果想知道线程是否正在运行任务,只需要调用Woker的tryLock方法,根据是否加锁成功就能判断,加锁成功说明当前线程没有加锁,也就没有执行任务了,在调用shutdown方法关闭线程池的时候,就用这种方式来判断线程有没有在执行任务,如果没有的话,来尝试打断没有执行任务的线程。