java线程池ThreadPoolExecutor实现原理
前言:
文章摘自 美团技术团队 《Java线程池实现原理及其在美团业务中的实践》
一、线程池
1.1、线程池是什么
线程池(ThreadPool)是一种基于池化思想管理线程的工具,常出现在多线程服务器中,如mysql。线程过多会带来额外的开销,其中包括创建销毁线程的开销、调度线程的开销等等,同时也降低了计算机的整体性能。线程池维护多个线程,等待监督管理者分配可并发执行的任务。这种做法,一方面避免了处理任务时创建销毁线程开销的代价,另一方面避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利用。
本文描述的线程池是 JDK 中提供的 ThreadPoolExecutor类。
使用线程池可以带来一系列好处:
- 降低资源消耗:池化重复利用已创建的线程,降低线程创建和销毁造成的损耗
- 提高响应速度:任务到达时,无须等待线程创建即可立即执行
- 提高线程的可管理性:线程是稀缺资源,线程的不合理分配还会导致资源调度失衡,降低系统稳定性,使用线程池可以进行统一的分配、调优和监控。
- 提供更多更强大的功功能:线程池具有可扩展性,允许向其中添加功能的功能了,比如延迟定时线程 ScheduledThreadPoolExecutor.
二、线程池核心设计与实现
前面提及到了线程池的思想,可以解决的问题以及拥有哪些优点,下面具体看看其详细的设计与实现。
2.1、总体设计
本文主要基于JDK1.8版本的 ThreadPoolExecutor 展开分析,首先看下该类的继承关系

ThreadPoolExecutor 的顶层父接口 Executor,其注释中有这样一段话,说明 Executor 提供了一种思想:将任务提交和任务执行解耦。用户无需关注如何执行创建线程,如何调度线程执行任务,主需要提供 Runnable 对象,
将任务执行逻辑提交到执行器(Executor)中,由 Executor 框架完成线程的调度和任务的执行部分。这也是JDK 线程池化的核心思想。
An object that executes submitted {@link Runnable} tasks. This interface provides a way of decoupling task submission from the mechanics of how each task will be run, including details of thread use, scheduling, etc.
ExecutorService 接口提供了一些能力:1)扩展执行任务的能力,补充可以为一个或者一批异步任务生成 Future 的方法;2)提供了管控线程池的方法,比如停止线程池的运行
AbstractExecutorService 则是上层的抽象类,将执行任务的流程串联了起来,保证下层的实现只需关注一个执行任务的方法即可。
ThreadPoolExecutor 实现最复杂的运行部分,ThreadPoolExecutor将会一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务。
ThreadPoolExecutor 的运行机制如下所示:

从上面的运行流程图可以看出,线程池内部实际上构建了一个生产者消费者模式,将线程和任务两者解耦,并不直接关联,从而良好的缓冲任务,复用线程。线程池的运行主要分成两部分:任务管理、线程管理。任务管理部分充当生产者的角色,当任务提交后,线程池会判断该任务后续的流转:1)直接申请线程执行任务; 2)缓冲到队列中等待线程执行; 3)拒绝该任务。线程管理部分是消费者,它们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收。
下面按照以下三部分详细讲解线程池运行机制:1、线程池如何维护自身状态 2、线程池如何管理任务 3、线程池如何管理线程
2.2、生命周期管理
线程池运行状态,并不是用于显示设置的,而是伴随着线程池的运行,由内部来维护。线程池内部使用一个变量维护两个值:运行状态(runState)和线程数量(workCount)。
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
ctl 对线程池的运行状态和线程池中有效线程的数量进行控制的字段,其高 3位保存 runState,低 29位保存workerCount,两个变量互不干扰。
内部封装的获取生命周期状态、获取线程池数量的计算方法如下所示
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; }
ThreadPoolExecutor的运行状态有5 中,分别是:
- RUNNING:能接受新提交的任务,并且也能处理阻塞队列中的任务
- SHUTDOWN:关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中以保存的任务
- STOP:不能接受新任务,也不再处理队列中的任务,会中断正在处理任务的线程
- TIDYING:所有的任务都已终止,workerCount 为 0
- TERMINATED:在terminated() 方法执行完成后进入该状态

2.3、任务执行机制
2.3.1、任务调度
任务调度是线程池的主要入口,当用于提交了一个任务,接下来这个任务将如何执行都是由这个阶段决定的。了解这部分就相当于了解了线程池的核心运行机制。
所有的任务调度都是由 execute 方法完成的,该方法主要是:检查现在线程池的运行状态、运行线程数、运行策略,决定接下来执行的流程,是直接申请线程执行,或是缓冲队列执行,亦或是直接拒绝该任务。
- 首先检测线程池运行状态,如果不是 RUNNING,则直接拒绝,线程池要保证在 RUNNING 的状态下执行任务
- 如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务
- 如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中
- 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务
- 如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常
其执行流程如下:

2.3.2、任务缓冲
任务缓冲模块是线程池能够管理任务的核心部分。线程池的本质是对任务和线程的管理,而做到这一点最关键的思想就是将任务和线程两者解耦,不让两者直接关联,才可以做后续的分配工作。
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
使用不同的队列可以实现不一样的任务存取策略,简单介绍下组合队列的成员:

2.3.3、任务申请
由上文的任务分配部分可知,任务的执行有两种可能:一种是任务直接由新创建的线程执行。另一种是线程从任务队列中获取任务然后执行,执行完任务的空闲线程会再次从队列中获取任务后执行。第一种情况仅出现在线程初始创建的时候,第二种是线程获取任务绝大多数的情况。
线程需要从任务缓存模块中不断地取任务执行,帮助线程从阻塞队列中获取任务,实现线程管理模块和任务管理模块之间的通信。这部分策略由getTask方法实现,其执行流程如下图所示:

getTask这部分进行了多次判断,为的是控制线程的数量,使其符合线程池的状态。如果线程池现在不应该持有那么多线程,则会返回null值。工作线程Worker会不断接收新任务去执行,而当工作线程Worker接收不到任务的时候,就会开始被回收。
2.3.4、任务拒绝
任务拒绝是线程池的保护部分,线程池有一个最大的容量,当线程池的任务缓存队列已满,并且线程池中的线程数目达到 maximumPoolSize时,就需要具体掉该任务,保护线程池。拒绝策略是一个接口:
public interface RejectedExecutionHandler { void rejectedExecution(Runnable r, ThreadPoolExecutor executor); }
用户可以通过实现这个接口去定制拒绝策略,可以选择JDK提供的四种已有拒绝策略如下,可以看到 ThreadPoolExecutor 的拒绝策略时抛出异常。

2.4、Worker 线程管理
2.4.1、worker线程
线程池为了掌控线程的状态并维护线程的生命周期,设计了线程池内的工作线程Worker。
private final class Worker extends AbstractQueuedSynchronizer implements Runnable { /** Thread this worker is running in. Null if factory fails. */ final Thread thread; /** Initial task to run. Possibly null. */ Runnable firstTask; /** Per-thread task counter */ volatile long completedTasks; }
Worker工作线程实现Runnable接口,持有一个线程 thread,一个初始化的任务 firstTask。thread可以用来执行任务,firsetTask用于保存传入的第一个任务,该任务可以为null。如果这个值非空,那么线程就会在启动初期立即执行这个任务,对应核心线程创建时的情况;如果值为null,就需要创建一个线程去执行列表中的任务,也就是非核心线程的创建。

线程池需要管理线程的生命周期,需要在线程长时间不运行的时候进行回收。线程池使用一张Hash表去持有线程的引用,这样可以通过添加引用、移除引用这样的操作来控制线程的生命周期。这个时候重要的就是如何判断线程是否在运行。
Worker是通过继承AQS,使用AQS来实现独占锁这个功能。没有使用可重入锁ReentrantLock,而是使用AQS,为的就是实现不可重入的特性去反应线程现在的执行状态。
- lock 方法一旦获取了独占锁,表示当前线程正在执行任务中
- 如果正在执行任务,则不应该中断线程
- 如果该线程现在不是独占锁的状态,也就是空闲的状态,说明它没有在处理任务,这时可以对该线程进行中断
- 线程池在执行shutdown方法或tryTerminate方法时会调用interruptIdleWorkers方法来中断空闲的线程,interruptIdleWorkers方法会使用tryLock方法来判断线程池中的线程是否是空闲状态;如果线程是空闲状态则可以安全回收。

2.4.2、Worker线程增加
增加线程是通过线程池的 addWorker 方法,该方法功能仅仅是增加一个线程,分配线程的策略是在上个步骤完成的。addWorker 的两个参数:firsetTask、core。

2.4.3、线程回收
线程池中的线程销毁依赖 JVM 自动回收,线程池做的工作是根据当前线程池的状态维护一定数量的线程引用,防止被 JVM 回收,当需要回收时,消除其引用即可。
Worker被创建出来后,就会不断地进行轮询,然后获取任务去执行,核心线程可以无限等待获取任务,非核心线程要限时获取任务。当Worker无法获取到任务,也就是获取的任务为空时,循环会结束,Worker会主动消除自身在线程池内的引用。
线程的回收工作在 processWorkerExit 中完成:

2.4.4、Worker线程执行任务
在Worker的run方法中调用了 runWorker方法来执行任务,runWorker 方法的执行过程如下:
- while循环不断的通过getTask() 方法获取任务
- getTask() 方法从阻塞队列中获取任务
- 如果线程正在停止,那么要保证当前线程是中断状态,否则要保证当前线程不是中断状态
- 执行任务
- 如果getTask() 结果为null 则跳出循环,执行 processWorkerExit() 方法,销毁线程

三、Todo:线程池核心参数的动态配置
ThreadPoolExecutor.setCorePoolSize(n)
四、Todo:线程池监听
ThreadPoolExecutor.beforeExecute(Thread t, Runnable r);
ThreadPoolExecutor.afterExecute(Thread t, Runnable r);

浙公网安备 33010602011771号