线程池是怎样工做的

咱们在工做中或多或少都使用过线程池,可是为何要使用线程池呢?从他的名字中咱们就应该知道,线程池使用了一种池化技术,和不少其余池化技术同样,都是为了更高效的利用资源,例如连接池,内存池等等。数据库

数据库连接是一种很昂贵的资源,建立和销毁都须要付出高昂的代价,为了不频繁的建立数据库连接,因此产生了连接池技术。优先在池子中建立一批数据库连接,有须要访问数据库时,直接到池子中去获取一个可用的连接,使用完了以后再归还到连接池中去。缓存

一样的,线程也是一种宝贵的资源,而且也是一种有限的资源,建立和销毁线程也一样须要付出不菲的代价。咱们全部的代码都是由一个一个的线程支撑起来的,现在的芯片架构也决定了咱们必须编写多线程执行的程序,以获取最高的程序性能。多线程

那么怎样高效的管理多线程之间的分工与协做就成了一个关键问题,Doug Lea 大神为咱们设计并实现了一款线程池工具,经过该工具就能够实现多线程的能力,并实现任务的高效执行与调度。架构

为了正确合理的使用线程池工具,咱们有必要对线程池的原理进行了解。工具

本篇文章主要从三个方面来对线程池进行分析:线程池状态、重要属性、工做流程。性能

线程池状态

首先线程池是有状态的,这些状态标识这线程池内部的一些运行状况,线程池的开启到关闭的过程就是线程池状态的一个流转的过程。this

线程池共有五种状态:线程

thread-pool-executor-status.jpg

状态 含义
RUNNING 运行状态,该状态下线程池能够接受新的任务,也能够处理阻塞队列中的任务<br />执行 shutdown 方法可进入 SHUTDOWN 状态<br />执行 shutdownNow 方法可进入 STOP 状态
SHUTDOWN 待关闭状态,再也不接受新的任务,继续处理阻塞队列中的任务<br />当阻塞队列中的任务为空,而且工做线程数为0时,进入 TIDYING 状态
STOP 中止状态,不接收新任务,也不处理阻塞队列中的任务,而且会尝试结束执行中的任务<br />当工做线程数为0时,进入 TIDYING 状态
TIDYING 整理状态,此时任务都已经执行完毕,而且也没有工做线程<br />执行 terminated 方法后进入 TERMINATED 状态
TERMINATED 终止状态,此时线程池彻底终止了,并完成了全部资源的释放

重要属性

一个线程池的核心参数有不少,每一个参数都有着特殊的做用,各个参数聚合在一块儿后将完成整个线程池的完整工做。设计

一、线程状态和工做线程数量

首先线程池是有状态的,不一样状态下线程池的行为是不同的,5种状态已经在上面说过了。对象

另外线程池确定是须要线程去执行具体的任务的,因此在线程池中就封装了一个内部类 Worker 做为工做线程,每一个 Worker 中都维持着一个 Thread。

线程池的重点之一就是控制线程资源合理高效的使用,因此必须控制工做线程的个数,因此须要保存当前线程池中工做线程的个数。

看到这里,你是否以为须要用两个变量来保存线程池的状态和线程池中工做线程的个数呢?可是在 ThreadPoolExecutor 中只用了一个 AtomicInteger 型的变量就保存了这两个属性的值,那就是 ctl。

ctl.jpg

ctl 的高3位用来表示线程池的状态(runState),低29位用来表示工做线程的个数(workerCnt),为何要用3位来表示线程池的状态呢,缘由是线程池一共有5种状态,而2位只能表示出4种状况,因此至少须要3位才能表示得了5种状态。

二、核心线程数和最大线程数

如今有了标志工做线程的个数的变量了,那到底该有多少个线程才合适呢?线程多了浪费线程资源,少了又不能发挥线程池的性能。

为了解决这个问题,线程池设计了两个变量来协做,分别是:

  • 核心线程数:corePoolSize 用来表示线程池中的核心线程的数量,也能够称为可闲置的线程数量
  • 最大线程数:maximumPoolSize 用来表示线程池中最多可以建立的线程数量

如今咱们有一个疑问,既然已经有了标识工做线程的个数的变量了,为何还要有核心线程数、最大线程数呢?

其实你这样想就可以理解了,建立线程是有代价的,不能每次要执行一个任务时就建立一个线程,可是也不能在任务很是多的时候,只有少许的线程在执行,这样任务是来不及处理的,而是应该建立合适的足够多的线程来及时的处理任务。随着任务数量的变化,当任务数明显很小时,本来建立的多余的线程就没有必要再存活着了,由于这时使用少许的线程就可以处理的过来了,因此说真正工做的线程的数量,是随着任务的变化而变化的。

那核心线程数和最大线程数与工做线程个数的关系是什么呢?

core-maximum-pool-size.jpg

工做线程的个数可能从0到最大线程数之间变化,当执行一段时间以后可能维持在 corePoolSize,但也不是绝对的,取决于核心线程是否容许被超时回收。

三、建立线程的工厂

既然是线程池,那天然少不了线程,线程该如何来建立呢?这个任务就交给了线程工厂 ThreadFactory 来完成。

四、缓存任务的阻塞队列

上面咱们说了核心线程数和最大线程数,而且也介绍了工做线程的个数是在0和最大线程数之间变化的。可是不可能一会儿就建立了全部线程,把线程池装满,而是有一个过程,这个过程是这样的:

当线程池接收到一个任务时,若是工做线程数没有达到corePoolSize,那么就会新建一个线程,并绑定该任务,直到工做线程的数量达到 corePoolSize 前都不会重用以前的线程。

当工做线程数达到 corePoolSize 了,这时又接收到新任务时,会将任务存放在一个阻塞队列中等待核心线程去执行。为何不直接建立更多的线程来执行新任务呢,缘由是核心线程中极可能已经有线程执行完本身的任务了,或者有其余线程立刻就能处理完当前的任务,而且接下来就能投入到新的任务中去,因此阻塞队列是一种缓冲的机制,给核心线程一个机会让他们充分发挥本身的能力。另一个值得考虑的缘由是,建立线程毕竟是比较昂贵的,不可能一有任务要执行就去建立一个新的线程。

因此咱们须要为线程池配备一个阻塞队列,用来临时缓存任务,这些任务将等待工做线程来执行。

work-queue.jpg

五、非核心线程存活时间

上面咱们说了当工做线程数达到 corePoolSize 时,线程池会将新接收到的任务存放在阻塞队列中,而阻塞队列又两种状况:一种是有界的队列,一种是无界的队列。

若是是无界队列,那么当核心线程都在忙的时候,全部新提交的任务都会被存放在该无界队列中,这时最大线程数将变得没有意义,由于阻塞队列不会存在被装满的状况。

若是是有界队列,那么当阻塞队列中装满了等待执行的任务,这时再有新任务提交时,线程池就须要建立新的“临时”线程来处理,至关于增派人手来处理任务。

可是建立的“临时”线程是有存活时间的,不可能让他们一直都存活着,当阻塞队列中的任务被执行完毕,而且又没有那么多新任务被提交时,“临时”线程就须要被回收销毁,在被回收销毁以前等待的这段时间,就是非核心线程的存活时间,也就是 keepAliveTime 属性。

那么什么是“非核心线程”呢?是否是先建立的线程就是核心线程,后建立的就是非核心线程呢?

其实核心线程跟建立的前后没有关系,而是跟工做线程的个数有关,若是当前工做线程的个数大于核心线程数,那么全部的线程均可能是“非核心线程”,都有被回收的可能。

一个线程执行完了一个任务后,会去阻塞队列里面取新的任务,在取到任务以前它就是一个闲置的线程。

取任务的方法有两种,一种是经过 take() 方法一直阻塞直到取出任务,另外一种是经过 poll(keepAliveTime,timeUnit) 方法在必定时间内取出任务或者超时,若是超时这个线程就会被回收,请注意核心线程通常不会被回收。

那么怎么保证核心线程不会被回收呢?仍是跟工做线程的个数有关,每个线程在取任务的时候,线程池会比较当前的工做线程个数与核心线程数:

  • 若是工做线程数小于当前的核心线程数,则使用第一种方法取任务,也就是没有超时回收,这时全部的工做线程都是“核心线程”,他们不会被回收;
  • 若是大于核心线程数,则使用第二种方法取任务,一旦超时就回收,因此并无绝对的核心线程,只要这个线程没有在存活时间内取到任务去执行就会被回收。

因此每一个线程想要保住本身“核心线程”的身份,必须充分努力,尽量快的获取到任务去执行,这样才能逃避被回收的命运。

核心线程通常不会被回收,可是也不是绝对的,若是咱们设置了容许核心线程超时被回收的话,那么就没有核心线程这种说法了,全部的线程都会经过 poll(keepAliveTime, timeUnit) 来获取任务,一旦超时获取不到任务,就会被回收,通常不多会这样来使用,除非该线程池须要处理的任务很是少,而且频率也不高,不须要将核心线程一直维持着。

六、拒绝策略

虽然咱们有了阻塞队列来对任务进行缓存,这从必定程度上为线程池的执行提供了缓冲期,可是若是是有界的阻塞队列,那就存在队列满的状况,也存在工做线程的数据已经达到最大线程数的时候。若是这时候再有新的任务提交时,显然线程池已经爱莫能助了,由于既没有空余的队列空间来存放该任务,也没法建立新的线程来执行该任务了,因此这时咱们就须要有一种拒绝策略,即 handler。

拒绝策略是一个 RejectedExecutionHandler 类型的变量,用户能够自行指定拒绝的策略,若是不指定的话,线程池将使用默认的拒绝策略:抛出异常。

在线程池中还为咱们提供了不少其余能够选择的拒绝策略:

  • 直接丢弃该任务
  • 使用调用者线程执行该任务
  • 丢弃任务队列中的最老的一个任务,而后提交该任务

工做流程

了解了线程池中全部的重要属性以后,如今咱们须要来了解下线程池的工做流程了。

how-thread-pool-work.jpg

上图是一张线程池工做的精简图,实际的过程比这个要复杂的多,不过这些应该可以彻底覆盖到线程池的整个工做流程了。

整个过程能够拆分红如下几个部分:

一、提交任务

当向线程池提交一个新的任务时,线程池有三种处理状况,分别是:建立一个工做线程来执行该任务、将任务加入阻塞队列、拒绝该任务。

提交任务的过程也能够拆分红如下几个部分:

  • 当工做线程数小于核心线程数时,直接建立新的核心工做线程
  • 当工做线程数不小于核心线程数时,就须要尝试将任务添加到阻塞队列中去
  • 若是可以加入成功,说明队列尚未满,那么须要作如下的二次验证来保证添加进去的任务可以成功被执行
    • 验证当前线程池的运行状态,若是是非RUNNING状态,则须要将任务从阻塞队列中移除,而后拒绝该任务
    • 验证当前线程池中的工做线程的个数,若是为0,则须要主动添加一个空工做线程来执行刚刚添加到阻塞队列中的任务
  • 若是加入失败,则说明队列已经满了,那么这时就须要建立新的“临时”工做线程来执行任务
    • 若是建立成功,则直接执行该任务
    • 若是建立失败,则说明工做线程数已经等于最大线程数了,则只能拒绝该任务了

整个过程能够用下面这张图来表示:

execute-runnable.jpg

二、建立工做线程

建立工做线程须要作一系列的判断,须要确保当前线程池能够建立新的线程以后,才能建立。

首先,当线程池的状态是 SHUTDOWN 或者 STOP 时,则不能建立新的线程。

另外,当线程工厂建立线程失败时,也不能建立新的线程。

还有就是当前工做线程的数量与核心线程数、最大线程数进行比较,若是前者大于后者的话,也不容许建立。

除此以外,会尝试经过 CAS 来自增工做线程的个数,若是自增成功了,则会建立新的工做线程,即 Worker 对象。

而后加锁进行二次验证是否可以建立工做线程,最后若是建立成功,则会启动该工做线程。

三、启动工做线程

当工做线程建立成功后,也就是 Worker 对象已经建立好了,这时就须要启动该工做线程,让线程开始干活了,Worker 对象中关联着一个 Thread,因此要启动工做线程的话,只要经过 worker.thread.start() 来启动该线程便可。

启动完了以后,就会执行 Worker 对象的 run 方法,由于 Worker 实现了 Runnable 接口,因此本质上 Worker 也是一个线程。

经过线程 start 开启以后就会调用到 Runnable 的 run 方法,在 worker 对象的 run 方法中,调用了 runWorker(this) 方法,也就是把当前对象传递给了 runWorker 方法,让他来执行。

四、获取任务并执行

在 runWorker 方法被调用以后,就是执行具体的任务了,首先须要拿到一个能够执行的任务,而 Worker 对象中默认绑定了一个任务,若是该任务不为空的话,那么就是直接执行。

执行完了以后,就会去阻塞队列中获取任务来执行,而获取任务的过程,须要考虑当前工做线程的个数。

  • 若是工做线程数大于核心线程数,那么就须要经过 poll 来获取,由于这时须要对闲置的线程进行回收;
  • 若是工做线程数小于等于核心线程数,那么就能够经过 take 来获取了,所以这时全部的线程都是核心线程,不须要进行回收,前提是没有设置 allowCoreThreadTimeOut

逅弈逐码,专一于原创分享,用通俗易懂的图文描述源码及原理

相关文章
相关标签/搜索