线程池的基本概念

线程池,是一种线程的使用模式,它为了下降线程使用中频繁的建立和销毁所带来的资源消耗与代价。
经过建立必定数量的线程,让他们时刻准备就绪等待新任务的到达,而任务执行结束以后再从新回来继续待命。java

这就是线程池最核心的设计思路,「复用线程,平摊线程的建立与销毁的开销代价」。git

相比于来一个任务建立一个线程的方式,使用线程池的优点体如今以下几点:程序员

  1. 避免了线程的重复建立与开销带来的资源消耗代价
  2. 提高了任务响应速度,任务来了直接选一个线程执行而无需等待线程的建立
  3. 线程的统一分配和管理,也方便统一的监控和调优

线程池的实现天生就实现了异步任务接口,容许你提交多个任务到线程池,线程池负责选用线程执行任务调度。github

异步任务在上一篇文章中已经作过一点铺垫介绍,那么本篇就在前一篇的基础上深刻的去探讨一下异步任务与线程池的相关内容。微信

基本介绍

在正式介绍线程池相关概念以前,咱们先看一张线程池相关接口的类图结构,网上盗来的,但画的仍是很全面的。异步

线程池相关类图

右上角的几个接口能够先不看,等咱们介绍到组合任务的时候会继续说的,咱们看左边,Executor、ExecutorService 以及 AbstractExecutorService 都是咱们熟悉的,它们抽象了任务执行者的基本模型。函数

ThreadPoolExecutor 是对线程池概念的抽象,它天生实现了任务执行的相关接口,也就是说,线程池也是一个任务的执行者,容许你向其中提交多个任务,线程池将负责分配线程与调度任务。学习

至于 Schedule 线程池,它是扩展了基础的线程池实现,提供「计划调度」能力,定时调度任务,延时执行等。this

线程池基本原理

ThreadPoolExecutor 的建立并不复杂,直接 new 就好,只不过构造函数有很久个重载,咱们直接看最底层的那个,也就是参数最多的那个。线程

public ThreadPoolExecutor
(   int corePoolSize,
    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue,
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler
)

建立一个线程池须要传这么多参数?是否是以为有点丧心病狂?

不要担忧,我说了,这是最复杂的一个构造函数重载,须要传入最全面的构造参数。而你平常使用时,固然可使用 ThreadPoolExecutor 中的其余较为简便的构造函数,只不过有些你没传的参数将配置为默认值而已。

下面咱们将从这些参数的含义出发,看看线程池 ThreadPoolExecutor 具有一个怎样的构成结构。

一、线程池容量问题

构造函数中有这么几个参数是用于配置线程池中线程容量与生命周期的:

  • corePoolSize
  • maximumPoolSize
  • keepAliveTime

corePoolSize 指定了线程池中的核心线程的个数,核心线程就是永远不会被销毁的线程,一旦被建立出来就将永远存活在线程池之中。

maximumPoolSize 指定了线程池可以建立的最大线程数量。

keepAliveTime 是用于控制非核心线程最长空闲等待时间,若是一个非核心线程处理完任务后回到线程池待命,超过这个指定时长依然没有新任务的分配将致使线程被销毁。

二、任务阻塞问题

ThreadPoolExecutor 中有这么一个字段:

private final BlockingQueue workQueue;

这个队列的做用很明显,就是当线程池中的线程不够用的时候,让任务排队,等待有线程空闲再来取任务去执行。

三、线程工厂

线程工厂 ThreadFactory 中只定义了一个方法 newThread,子类实现它并按照本身的需求建立一个线程返回。

例如 DefaultThreadFactory 实现的该方法将建立一个线程,名称格式: pool- <线程池编号> -thread- <线程编号> ,设置线程的优先级为标准优先级,非守护线程等。

四、任务拒绝策略

构造函数中还有一个参数 handle 是必须传的,它将为 ThreadPoolExecutor 中的同名字段赋值。

private volatile RejectedExecutionHandler handler;

RejectedExecutionHandler 中定义了一个 rejectedExecution 用于描述一种任务拒绝策略。那么哪一种状况下才会触发该方法的调用呢?

当线程池中的全部线程所有分配出去工做了,而且任务阻塞队列也阻塞满了,那么此时新提交的任务将触发任务拒绝策略

而拒绝策略主要有如下四个子类实现,而它们都是定义在 ThreadPoolExecutor 的内部类,咱们看一看都是哪四种策略:

  • AbortPolicy
  • CallerRunsPolicy
  • DiscardOldestPolicy
  • DiscardPolicy

AbortPolicy 是默认的拒绝策略,他的实现就是直接抛出 RejectedExecutionException 异常。

CallerRunsPolicy 暂停当前提交任务的线程返回,本身去执行本身提交过来的任务。

DiscardOldestPolicy 策略将从阻塞任务队列对头移除一个任务并将本身排到队列尾部等待调度执行。

DiscardPolicy 是一种佛系策略,方法体的实现为空,什么也不作,也即忽略当前任务的提交。

这样,咱们零零散散的对线程池的内部有了一个基本的认识,下面咱们要把这些都串起来,看一看源码。从一个任务的提交,到分配到线程执行任务,一整个过程的相关逻辑作一个探究。

看一看源码

先来看一看任务的提交方法,submit

submit

以前的文章咱们也说过,这个 submit 方法有四个重载,分别容许你传入不一样类型的任务,Runnable 或是 Callable。咱们这里就之前者为例。

这个 RunnableFuture 类型咱们以前说过,他只不过是同时继承了 Runnable 和 Future 接口,象征性的描述了「这是一个可监控的任务」。

而后你会发现,整个 submit 的核心逻辑在 execute 方法里面,也就是说 execute 方法才是真正向线程池提交任务的方法。咱们重点看一看这个 execute 方法。

先看看 ThreadPoolExecutor 中定义几个重要的字段:

image

ctl 是一个原子变量,它用了一个 32 位的整型描述了两个重要信息。当前线程池运行状态(runState)和当前线程池中有效的线程个数(workCount)。

runState 占用高 3 比特位,workCount 占用低 29 比特位。

接着咱们来看 execute 方法的实现:

image

红框部分:

若是当前线程池中的实际工做线程数还未达到配置的核心线程数量,那么将调用 addWorker 为当前任务建立一个新线程并启动执行。

addWorker 方法代码仍是有点多的,这里就截图出来进行分析了,由于并不难,咱们总结下该方法的逻辑:

  1. 死循环中判断线程池状态是否正常,若是不正常被关闭了等,将直接返回 false
  2. 若是正常则 CAS 尝试为 workerCount 增长一,并建立一个新的线程调用 start 方法执行任务。

不知道你留意到 addWorker 方法的第二个参数了没有,这个参数用于指定线程池的上界。

若是传的是 true,则说明使用 corePoolSize 做为上界,也就是这次为任务分配线程若是线程池中全部的工做线程数达到这个 corePoolSize 则将拒绝分配并返回添加失败。

若是传的是 false,则使用 maximumPoolSize 做为上界,道理是同样的。

蓝框部分:

从红框出来,你能够认为任务分配线程失败了,大几率是全部正常工做的线程数达到核心线程数量了。这部分作的事情就是:

  1. 若是线程池状态正常,就尝试将当前任务添加到任务阻塞队列上。
  2. 再一次检查线程池状态,若是异常了,将撤回刚才添加的任务并根据咱们设定的拒绝策略予以拒绝。
  3. 若是发现线程池自上次检查后,所哟线程所有死亡,那么将建立一个空闲线程,适当的时候他会去从任务队列取咱们刚刚添加的任务的

黄框部分:

到达黄色部分必然说明线程池状态异常或是队列添加失败,大几率是由于队列满了没法再添加了。

此时再次调用 addWorker 方法,不过此次传入 false,意思是,我知道全部的核心线程都在忙而且任务队列也排满了,那么你就额外建立一个非核心线程来执行个人任务吧。

若是失败了,执行拒绝策略。

咱们总结一下任务的提交到分配线程,甚至阻塞到任务队列这一系列过程:

一个任务过来,若是线程池中的线程数不足咱们配置的核心线程数,那么会尝试建立新线程来执行任务,不然会优先把任务往阻塞队列上添加

若是阻塞队列上满员了,那么说明当前线程池中核心线程工做量有点大,将开始建立非核心线程共同执行任务,直到达到上限或是阻塞队列再也不满员。

到这里呢,咱们对于任务的提交与线程分配已经有了一个基本的认识了,相信你也必定好奇当一个线程的任务执行结束以后,他是如何去取下一个任务的。

这部分咱们也来分析分析

线程池的内部定义了一个 Worker 内部类,这个类有两个字段,一个用于保存当前的任务,一个用于保存用于执行该任务的线程。

addWorker 中会调用线程的 start 方法,进而会执行 Worker 实例的 run 方法,这个 run 方法是这样的:

public void run() {
    runWorker(this);
}

runWorker 很长,就不截出来一点点分析了,我总结下他的实现逻辑:

  1. 若是本身内部的任务是空,则尝试从阻塞队列上获取一个任务
  2. 执行任务
  3. 循环的执行 1和2 两个步骤,直到阻塞队列中没有任务可获取
  4. 调用 processWorkerExit 方法移除当前线程在线程池中的引用,也就至关于销毁了一个线程,由于不久后会被 GC 回收

可是这里有一个细节和你们说一下,第一个步骤从任务队列中取一个任务调用的是 getTask 方法。

这个方法设定了一个逻辑,若是线程池中正在工做的线程数大于设定的核心线程数,也就是说线程池中存在非核心线程,那么当前线程获取任务时,若是超过指定时长依然没有获取,就将返回跳过循环执行咱们 runWorker 的第四个步骤,移除对该线程的引用。

反之,若是此时有效工做线程数少于规定的核心线程数,则认定当前线程是一个核心线程,因而对于获取任务失败的处理是「阻塞到条件队列上,等待其余线程唤醒」。

何时唤醒也很容易想到了,就是当任务队列有新任务添加时,会唤醒全部的核心线程,他们会去队列上取任务,没抢到的依然回去阻塞。

至此,线程池相关的内容介绍完毕,有些方法的实现我只是总结了大概的逻辑,具体的尤待大家本身去探究,有问题也欢迎你和我讨论。

关注公众不迷路,一个爱分享的程序员。

公众号回复「1024」加做者微信一块儿探讨学习!

每篇文章用到的全部案例代码素材都会上传我我的 github

https://github.com/SingleYam/overview_java

欢迎来踩!

YangAM 公众号

相关文章
相关标签/搜索