参考:https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html
一、线程池是什么、有什么用
线程池是基于池化思想来管理线程的工具。当系统线程过多,就会增强一系列开销,包括线程的创建与销毁、cpu的调度等等,使用线程池对任务进行缓冲,使线程可以重用,降低了开销。
线程过多会带来额外的开销,其中包括创建销毁线程的开销、调度线程的开销等等,同时也降低了计算机的整体性能。线程池维护多个线程,等待监督管理者分配可并发执行的任务。这种做法,一方面避免了处理任务时创建销毁线程开销的代价,另一方面避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利用。
池化思想:内存池、常量池、连接池(数据库)、实例池
二、线程池的核心实现
Executor:将任务提交与任务执行解耦,用户无需关注如何创建线程只需提交实现了Runable结构的对象即可。
ExecutorService、AbstractExecutorService:扩充执行任务的管理。
ThreadPoolExecutor:维护自身的生命周期、管理线程与任务
这里需要说一下自己理解的线程池的消费者生产者模式:任务的放入需要放入阻塞队列,相当于生产者,线程需要消耗阻塞队列里面的任务,相当于消费者。
1、内存池自身状态管理:
用了一个原子类来表示自身状态与线程数量两个值,其中高三位表示自身状态,其他位用来保存线程数量。
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
内存状态有五种:RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED
2、任务管理:
任务管理包括任务调度、任务缓冲、任务申请、任务拒绝。
2.1 任务管理即从一个任务的视角出来观察线程池的运行状态
1、首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。
2、如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。
3、如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
4、如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。
5、如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。
2.2 任务缓冲是任务调度中的一部分,但是这部分的意义很大,他把任务与线程进行了解耦,他们都与阻塞队列相关联了。
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
常用的堵塞队列:ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue
2.3 任务申请
任务申请有两种形式:
- 任务直接由新创建的线程执行。(一般都是刚开始的时候)
- 线程从任务队列中获取任务然后执行,执行完任务的空闲线程会再次去从队列中申请任务再去执行。(大多数都是这种)
2.4 任务拒绝
线程池不可能无限大,线程数有个上限,因此当池中的线程数达到最大限度时就需要采用某种策略拒绝掉该任务,保护线程池。
拒绝策略是一个接口,可以重写。
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
常用拒绝策略:
- ThreadPoolExecutor.AbortPolicy:直接抛出异常
- ThreadPoolExecutor.DiscardPolicy:丢弃任务
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后提交任务
- ThreadPoolExecutor.CallerRunsPolicy:谁提交谁处理
3、线程管理
线程管理主要是线程本身的介绍,然后就是添加、回收以及执行任务。
3.1 Worker线程:
private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
final Thread thread;//Worker持有的线程
Runnable firstTask;//初始化的任务,可以为null
}
Worker这个工作线程,实现了Runnable接口,并持有一个线程thread,一个初始化的任务firstTask。thread是在调用构造方法时通过ThreadFactory来创建的线程,可以用来执行任务;firstTask用它来保存传入的第一个任务,这个任务可以有也可以为null。如果这个值是非空的,那么线程就会在启动初期立即执行这个任务,也就对应核心线程创建时的情况;如果这个值是null,那么就需要创建一个线程去执行任务列表(workQueue)中的任务,也就是非核心线程的创建。
这里非常有意思的是worker线程的周期管理,线程创建之后的生命状态一般都是回收,回收的话要判断线程是否处于空闲状态,如果是才能回收。
所以Worker继承了AQS,使用AQS来实现独占锁这个功能。没有使用可重入锁ReentrantLock,而是使用AQS,为的就是实现不可重入的特性去反应线程现在的执行状态。
- lock方法一旦获取了独占锁,表示当前线程正在执行任务中。
- 如果正在执行任务,则不应该中断线程。
- 如果该线程现在不是独占锁的状态,也就是空闲的状态,说明它没有在处理任务,这时可以对该线程进行中断。
- 线程池在执行shutdown方法或tryTerminate方法时会调用interruptIdleWorkers方法来中断空闲的线程,interruptIdleWorkers方法会使用tryLock方法来判断线程池中的线程是否是空闲状态;如果线程是空闲状态则可以安全回收。
3.2 Worker线程增加
增加线程是通过线程池中的addWorker方法,该方法的功能就是增加一个线程,该方法不考虑线程池是在哪个阶段增加的该线程,这个分配线程的策略是在上个步骤完成的,该步骤仅仅完成增加线程,并使它运行,最后返回是否成功这个结果。addWorker方法有两个参数:firstTask、core。firstTask参数用于指定新增的线程执行的第一个任务,该参数可以为空;core参数为true表示在新增线程时会判断当前活动线程数是否少于corePoolSize,false表示新增线程前需要判断当前活动线程数是否少于maximumPoolSize,其执行流程如下图所示(https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html):
3.3 Worker线程回收
线程池中线程的销毁依赖JVM自动的回收,线程池做的工作是根据当前线程池的状态维护一定数量的线程引用,防止这部分线程被JVM回收,当线程池决定哪些线程需要回收时,只需要将其引用消除即可。Worker被创建出来后,就会不断地进行轮询,然后获取任务去执行,核心线程可以无限等待获取任务,非核心线程要限时获取任务。当Worker无法获取到任务,也就是获取的任务为空时,循环会结束,Worker会主动消除自身在线程池内的引用。
3.4 Worker线程执行任务
在Worker类中的run方法调用了runWorker方法来执行任务,runWorker方法的执行过程如下:
1.while循环不断地通过getTask()方法获取任务。
2.getTask()方法从阻塞队列中取任务。
3.如果线程池正在停止,那么要保证当前线程是中断状态,否则要保证当前线程不是中断状态。
4.执行任务。
5.如果getTask结果为null则跳出循环,执行processWorkerExit()方法,销毁线程。
图(https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html)
三、线程池在业务中的应用
需要根据不同需求进行讨论
- 快速响应用户请求
描述:用户发起的实时请求,服务追求响应时间。比如说用户要查看一个商品的信息,那么我们需要将商品维度的一系列信息如商品的价格、优惠、库存、图片等等聚合起来,展示给用户。
分析:从用户体验角度看,这个结果响应的越快越好,如果一个页面半天都刷不出,用户可能就放弃查看这个商品了。而面向用户的功能聚合通常非常复杂,伴随着调用与调用之间的级联、多级级联等情况,业务开发同学往往会选择使用线程池这种简单的方式,将调用封装成任务并行的执行,缩短总体响应时间。另外,使用线程池也是有考量的,这种场景最重要的就是获取最大的响应速度去满足用户,所以应该不设置队列去缓冲并发任务,调高corePoolSize和maxPoolSize去尽可能创造多的线程快速执行任务。
思考:这类其实就是io密集型任务,大部分时间浪费在io上面了,因此可以扩大线程池的线程上限。
- 快速处理批量任务
描述:离线的大量计算任务,需要快速执行。比如说,统计某个报表,需要计算出全国各个门店中有哪些商品有某种属性,用于后续营销策略的分析,那么我们需要查询全国所有门店中的所有商品,并且记录具有某属性的商品,然后快速生成报表。
分析:这种场景需要执行大量的任务,我们也会希望任务执行的越快越好。这种情况下,也应该使用多线程策略,并行计算。但与响应速度优先的场景区别在于,这类场景任务量巨大,并不需要瞬时的完成,而是关注如何使用有限的资源,尽可能在单位时间内处理更多的任务,也就是吞吐量优先的问题。所以应该设置队列去缓冲并发任务,调整合适的corePoolSize去设置处理任务的线程数。在这里,设置的线程数过多可能还会引发线程上下文切换频繁的问题,也会降低处理任务的速度,降低吞吐量。
思考:这种就是cpu密集型任务,因此可以使用阻塞队列对线程进行缓冲,然后用相对较少的线程去执行,因为减少了上下文切换的时间,因此cpu的利用率较高。
初次之外还有参数设置公式、参数动态化等方法来调整线程池以适用于生产开发。