线程池
线程池概述
线程池是一种基于池化思想管理线程的工具,经常出现在多线程服务器上,比如MySQL
池化,顾名思义,是为了最大化收益并最小化风险,而将资源统一在一起管理的一种思想。
由于存在许多的短时间任务,所以线程不断创建和销毁会有额外的代价,另外线程过多就会导致数量膨胀导致过分调度问题,而线程池就能够保证对内核的充分利用,还有一些其它的好处,例如:
- **降低资源消耗:**通过池化技术重复利用已经创建线程,降低线程创建和销毁的代价。
- **提高响应速度:**任务到达的时候,跳过了创建线程的步骤,可以立即执行。
- **提高了线程的可管理性:**线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
- 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。
并发环境下,系统不知道有多少任务需要执行(在任意时刻下),也不知道要给多少资源,就有了很多问题:
- 一直需要申请、销毁资源和调度资源,会带来可能非常巨大的消耗。
- 系统偏偏对这种一直申请资源的情况没有办法,可能会造成资源耗尽。
- 资源申请的多了,你这个线程来一点,那个来一点,就无法管理内部的资源分布,稳定性大大降低。
这时候就需要线程池了,在Java中的体现是ThreadPoolExecutor类。
线程池架构-总体设计
ThreadPoolExecutor类的继承关系为:
**顶层接口-Executor:**将任务提交和任务执行解耦,用户无需关注如何创建线程,如何调度,只需要提交任务的运行逻辑到Executor中即可。
**接口-ExecutorService:**增加了一些功能,概括为就是,为异步任务生成Future方法,并且提供了管控线程池的方法,比如停止啥的。
**抽象类-AbstractExecutorService:**上层的抽象类,将执行任务的流程串联了起来,保证下层的实现只需关注一个执行任务的方法即可。
**实现类-ThreadPoolExecutor:**实现最复杂的运行部分,ThreadPoolExecutor将会一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务。
ThreadPoolExecutor运行流程:
任务提交,判断是否立即执行或者拒绝任务,再或者让任务去缓冲队列呆着,就这三种状态。
执行任务的话就申请线程,当任务执行完之后,该线程就会继续获取新任务,当获取不到新任务时,线程回收。
线程池-生命周期管理
线程池的运行状态和池内线程数量,没有存在两个变量中,而是用一个32位Integer存储的,通过高3位保存运行状态,低29位保存线程数量,可以减少有改动的时候,需要对俩变量进行一直匹配,防止占用锁资源。线程池提供了很多方法去获取当前运行状态、个数,都是基于位运算的方法。
private static int runStateOf(int c) { return c & ~CAPACITY; } //计算当前运行状态
private static int workerCountOf(int c) { return c & CAPACITY; } //计算当前线程数量
private static int ctlOf(int rs, int wc) { return rs | wc; } //通过状态和线程数生成ctl
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
//ctl就是保存运行状态和数量的变量,通过ctlof获取,存入该AtomicInteger类型变量
以上就是线程池的五种运行状态,生命周期转换如图所示:
线程池-任务执行机制
任务调度
- 首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。
- 如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。
- 如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
- 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。
- 如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。
任务缓冲
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
具体有哪些阻塞队列,可见上述《阻塞队列》部分。
任务申请
任务的执行有两种可能:一种是任务直接由新创建的线程执行。另一种是线程从任务队列中获取任务然后执行,第一种情况仅出现在线程初始创建的时候,第二种是线程获取任务绝大多数的情况。
线程需要从任务缓存模块中不断地取任务执行,帮助线程从阻塞队列中获取任务,实现线程管理模块和任务管理模块之间的通信。这部分策略由getTask方法实现,其执行流程如下图所示:
任务拒绝
当线程池的任务缓存队列已满,并且线程池中的线程数目达到maximumPoolSize时,就需要拒绝掉该任务,采取任务拒绝策略,保护线程池。
可以通过实现RejectedExecutionHandler接口去自定义拒绝策略,也可以是用JDK提供的拒绝策略。
线程池-Worker线程管理
Worker线程
为了掌握线程的状态并维护线程的生命周期,设计了线程池内的工作线程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线程增加
增加线程是通过线程池中的addWorker方法,该方法的功能就是增加一个线程,该方法不考虑线程池是在哪个阶段增加的该线程,这个分配线程的策略是在上个步骤完成的,该步骤仅仅完成增加线程,并使它运行,最后返回是否成功这个结果。addWorker方法有两个参数:firstTask、core。firstTask参数用于指定新增的线程执行的第一个任务,该参数可以为空;core参数为true表示在新增线程时会判断当前活动线程数是否少于corePoolSize,false表示新增线程前需要判断当前活动线程数是否少于maximumPoolSize,其执行流程如下图所示:
worker线程回收
线程池中线程的销毁依赖JVM自动的回收,线程池做的工作是根据当前线程池的状态维护一定数量的线程引用,防止这部分线程被JVM回收,当线程池决定哪些线程需要回收时,只需要将其引用消除即可。
try {
while (task != null || (task = getTask()) != null) {
//执行任务
}
} finally {
processWorkerExit(w, completedAbruptly);//获取不到任务时,主动回收自己
}
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