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 方法完成的,该方法主要是:检查现在线程池的运行状态、运行线程数、运行策略,决定接下来执行的流程,是直接申请线程执行,或是缓冲队列执行,亦或是直接拒绝该任务。

  1. 首先检测线程池运行状态,如果不是 RUNNING,则直接拒绝,线程池要保证在 RUNNING 的状态下执行任务
  2. 如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务
  3. 如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中
  4. 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务
  5. 如果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,为的就是实现不可重入的特性去反应线程现在的执行状态。

  1. lock 方法一旦获取了独占锁,表示当前线程正在执行任务中
  2. 如果正在执行任务,则不应该中断线程
  3. 如果该线程现在不是独占锁的状态,也就是空闲的状态,说明它没有在处理任务,这时可以对该线程进行中断
  4. 线程池在执行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 方法的执行过程如下:

  1. while循环不断的通过getTask() 方法获取任务
  2. getTask() 方法从阻塞队列中获取任务
  3. 如果线程正在停止,那么要保证当前线程是中断状态,否则要保证当前线程不是中断状态
  4. 执行任务
  5. 如果getTask() 结果为null 则跳出循环,执行 processWorkerExit() 方法,销毁线程

三、Todo:线程池核心参数的动态配置

  ThreadPoolExecutor.setCorePoolSize(n)

四、Todo:线程池监听

  ThreadPoolExecutor.beforeExecute(Thread t, Runnable r);

  ThreadPoolExecutor.afterExecute(Thread t, Runnable r);

posted @ 2021-03-15 22:00  跬步-千里  阅读(184)  评论(0)    收藏  举报