线程池的基本概念

Posted yangming1996

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线程池的基本概念相关的知识,希望对你有一定的参考价值。

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

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

相比于来一个任务创建一个线程的方式,使用线程池的优势体现在如下几点:

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

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

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

基本介绍

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

技术分享图片

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

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

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

线程池基本原理

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

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

创建一个线程池需要传这么多参数?是不是觉得有点丧心病狂?

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

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

1、线程池容量问题

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

  • corePoolSize
  • maximumPoolSize
  • keepAliveTime

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

maximumPoolSize 指定了线程池能够创建的最大线程数量。

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

2、任务阻塞问题

ThreadPoolExecutor 中有这么一个字段:

private final BlockingQueue

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

3、线程工厂

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

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

4、任务拒绝策略

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

private volatile RejectedExecutionHandler handler;

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

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

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

  • AbortPolicy
  • CallerRunsPolicy
  • DiscardOldestPolicy
  • DiscardPolicy

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

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

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

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

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

看一看源码

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

技术分享图片

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

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

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

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

技术分享图片

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

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

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

技术分享图片

红框部分:

如果当前线程池中的实际工作线程数还未达到配置的核心线程数量,那么将调用 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

欢迎来踩!

技术分享图片






以上是关于线程池的基本概念的主要内容,如果未能解决你的问题,请参考以下文章

线程池的基本概念

newCacheThreadPool()newFixedThreadPool()newScheduledThreadPool()newSingleThreadExecutor()自定义线程池(代码片段

c# 并发编程系列之一:线程进程线程池的基本概念

性能基本概念

一个Windows下线程池的实现(C++)

线程池的基础与操作