随着计算机行业的飞速发展,摩尔定律逐渐失效,多核CPU成为主流。使用多线程并行计算逐渐成为开发人员提高服务器性能的基本武器。J.U.C提供的线程池:ThreadPoolExecutor类,帮助开发人员管理线程并方便地执行并行任务。了解并合理使用线程池,是一个开发人员必修的基本功。前端
本文开篇简述线程池概念和用途,接着结合线程池的源码,帮助读者领略线程池的设计思路,最后回归实践,经过案例讲述使用线程池遇到的问题,并给出了一种动态化线程池解决方案。算法
线程池(Thread Pool)是一种基于池化思想管理线程的工具,常常出如今多线程服务器中,如MySQL。数据库
线程过多会带来额外的开销,其中包括建立销毁线程的开销、调度线程的开销等等,同时也下降了计算机的总体性能。线程池维护多个线程,等待监督管理者分配可并发执行的任务。这种作法,一方面避免了处理任务时建立销毁线程开销的代价,另外一方面避免了线程数量膨胀致使的过度调度问题,保证了对内核的充分利用。编程
而本文描述线程池是JDK中提供的ThreadPoolExecutor类。后端
固然,使用线程池能够带来一系列好处:缓存
线程池解决的核心问题就是资源管理问题。在并发环境下,系统不可以肯定在任意时刻中,有多少任务须要执行,有多少资源须要投入。这种不肯定性将带来如下若干问题:安全
为解决资源分配这个问题,线程池采用了“池化”(Pooling)思想。池化,顾名思义,是为了最大化收益并最小化风险,而将资源统一在一块儿管理的一种思想。服务器
Pooling is the grouping together of resources (assets, equipment, personnel, effort, etc.) for the purposes of maximizing advantage or minimizing risk to the users. The term is used in finance, computing and equipment management.——wikipedia微信
“池化”思想不只仅能应用在计算机领域,在金融、设备、人员管理、工做管理等领域也有相关的应用。网络
在计算机领域中的表现为:统一管理IT资源,包括服务器、存储、和网络资源等等。经过共享资源,使用户在低投入中获益。除去线程池,还有其余比较典型的几种使用策略包括:
在了解完“是什么”和“为何”以后,下面咱们来一块儿深刻一下线程池的内部实现原理。
在前文中,咱们了解到:线程池是一种经过“池化”思想,帮助咱们管理线程而获取并发性的工具,在Java中的体现是ThreadPoolExecutor类。那么它的的详细设计与实现是什么样的呢?咱们会在本章进行详细介绍。
Java中的线程池核心实现类是ThreadPoolExecutor,本章基于JDK 1.8的源码来分析Java线程池的核心设计与实现。咱们首先来看一下ThreadPoolExecutor的UML类图,了解下ThreadPoolExecutor的继承关系。
ThreadPoolExecutor实现的顶层接口是Executor,顶层接口Executor提供了一种思想:将任务提交和任务执行进行解耦。用户无需关注如何建立线程,如何调度线程来执行任务,用户只需提供Runnable对象,将任务的运行逻辑提交到执行器(Executor)中,由Executor框架完成线程的调配和任务的执行部分。ExecutorService接口增长了一些能力:(1)扩充执行任务的能力,补充能够为一个或一批异步任务生成Future的方法;(2)提供了管控线程池的方法,好比中止线程池的运行。AbstractExecutorService则是上层的抽象类,将执行任务的流程串联了起来,保证下层的实现只需关注一个执行任务的方法便可。最下层的实现类ThreadPoolExecutor实现最复杂的运行部分,ThreadPoolExecutor将会一方面维护自身的生命周期,另外一方面同时管理线程和任务,使二者良好的结合从而执行并行任务。
ThreadPoolExecutor是如何运行,如何同时维护线程和执行任务的呢?其运行机制以下图所示:
线程池在内部实际上构建了一个生产者消费者模型,将线程和任务二者解耦,并不直接关联,从而良好的缓冲任务,复用线程。线程池的运行主要分红两部分:任务管理、线程管理。任务管理部分充当生产者的角色,当任务提交后,线程池会判断该任务后续的流转:(1)直接申请线程执行该任务;(2)缓冲到队列中等待线程执行;(3)拒绝该任务。线程管理部分是消费者,它们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收。
接下来,咱们会按照如下三个部分去详细讲解线程池运行机制:
线程池运行的状态,并非用户显式设置的,而是伴随着线程池的运行,由内部来维护。线程池内部使用一个变量维护两个值:运行状态(runState)和线程数量 (workerCount)。在具体实现中,线程池将运行状态(runState)、线程数量 (workerCount)两个关键参数的维护放在了一块儿,以下代码所示:
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
ctl
这个AtomicInteger类型,是对线程池的运行状态和线程池中有效线程的数量进行控制的一个字段, 它同时包含两部分的信息:线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount),高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; } //经过状态和线程数生成ctl
ThreadPoolExecutor的运行状态有5种,分别为:
其生命周期转换以下入所示:
2.3.1 任务调度
任务调度是线程池的主要入口,当用户提交了一个任务,接下来这个任务将如何执行都是由这个阶段决定的。了解这部分就至关于了解了线程池的核心运行机制。
首先,全部任务的调度都是由execute方法完成的,这部分完成的工做是:检查如今线程池的运行状态、运行线程数、运行策略,决定接下来执行的流程,是直接申请线程执行,或是缓冲到队列中执行,亦或是直接拒绝该任务。其执行过程以下:
其执行流程以下图所示:
2.3.2 任务缓冲
任务缓冲模块是线程池可以管理任务的核心部分。线程池的本质是对任务和线程的管理,而作到这一点最关键的思想就是将任务和线程二者解耦,不让二者直接关联,才能够作后续的分配工做。线程池中是以生产者消费者模式,经过一个阻塞队列来实现的。阻塞队列缓存任务,工做线程从阻塞队列中获取任务。
阻塞队列(BlockingQueue)是一个支持两个附加操做的队列。这两个附加的操做是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列经常使用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
下图中展现了线程1往阻塞队列中添加元素,而线程2从阻塞队列中移除元素:
使用不一样的队列能够实现不同的任务存取策略。在这里,咱们能够再介绍下阻塞队列的成员:
2.3.3 任务申请
由上文的任务分配部分可知,任务的执行有两种可能:一种是任务直接由新建立的线程执行。另外一种是线程从任务队列中获取任务而后执行,执行完任务的空闲线程会再次去从队列中申请任务再去执行。第一种状况仅出如今线程初始建立的时候,第二种是线程获取任务绝大多数的状况。
线程须要从任务缓存模块中不断地取任务执行,帮助线程从阻塞队列中获取任务,实现线程管理模块和任务管理模块之间的通讯。这部分策略由getTask方法实现,其执行流程以下图所示:
getTask这部分进行了屡次判断,为的是控制线程的数量,使其符合线程池的状态。若是线程池如今不该该持有那么多线程,则会返回null值。工做线程Worker会不断接收新任务去执行,而当工做线程Worker接收不到任务的时候,就会开始被回收。
2.3.4 任务拒绝
任务拒绝模块是线程池的保护部分,线程池有一个最大的容量,当线程池的任务缓存队列已满,而且线程池中的线程数目达到maximumPoolSize时,就须要拒绝掉该任务,采起任务拒绝策略,保护线程池。
拒绝策略是一个接口,其设计以下:
public interface RejectedExecutionHandler { void rejectedExecution(Runnable r, ThreadPoolExecutor executor); }
用户能够经过实现这个接口去定制拒绝策略,也能够选择JDK提供的四种已有拒绝策略,其特色以下:
2.4 Worker线程管理
2.4.1 Worker线程
线程池为了掌握线程的状态并维护线程的生命周期,设计了线程池内的工做线程Worker。咱们来看一下它的部分代码:
private final class Worker extends AbstractQueuedSynchronizer implements Runnable{ final Thread thread;//Worker持有的线程 Runnable firstTask;//初始化的任务,能够为null }
Worker这个工做线程,实现了Runnable接口,并持有一个线程thread,一个初始化的任务firstTask。thread是在调用构造方法时经过ThreadFactory来建立的线程,能够用来执行任务;firstTask用它来保存传入的第一个任务,这个任务能够有也能够为null。若是这个值是非空的,那么线程就会在启动初期当即执行这个任务,也就对应核心线程建立时的状况;若是这个值是null,那么就须要建立一个线程去执行任务列表(workQueue)中的任务,也就是非核心线程的建立。
Worker执行任务的模型以下图所示:
线程池须要管理线程的生命周期,须要在线程长时间不运行的时候进行回收。线程池使用一张Hash表去持有线程的引用,这样能够经过添加引用、移除引用这样的操做来控制线程的生命周期。这个时候重要的就是如何判断线程是否在运行。
Worker是经过继承AQS,使用AQS来实现独占锁这个功能。没有使用可重入锁ReentrantLock,而是使用AQS,为的就是实现不可重入的特性去反应线程如今的执行状态。
1.lock方法一旦获取了独占锁,表示当前线程正在执行任务中。
2.若是正在执行任务,则不该该中断线程。
3.若是该线程如今不是独占锁的状态,也就是空闲的状态,说明它没有在处理任务,这时能够对该线程进行中断。
4.线程池在执行shutdown方法或tryTerminate方法时会调用interruptIdleWorkers方法来中断空闲的线程,interruptIdleWorkers方法会使用tryLock方法来判断线程池中的线程是不是空闲状态;若是线程是空闲状态则能够安全回收。
在线程回收过程当中就使用到了这种特性,回收过程以下图所示:
2.4.2 Worker线程增长
增长线程是经过线程池中的addWorker方法,该方法的功能就是增长一个线程,该方法不考虑线程池是在哪一个阶段增长的该线程,这个分配线程的策略是在上个步骤完成的,该步骤仅仅完成增长线程,并使它运行,最后返回是否成功这个结果。addWorker方法有两个参数:firstTask、core。firstTask参数用于指定新增的线程执行的第一个任务,该参数能够为空;core参数为true表示在新增线程时会判断当前活动线程数是否少于corePoolSize,false表示新增线程前须要判断当前活动线程数是否少于maximumPoolSize,其执行流程以下图所示:
2.4.3 Worker线程回收
线程池中线程的销毁依赖JVM自动的回收,线程池作的工做是根据当前线程池的状态维护必定数量的线程引用,防止这部分线程被JVM回收,当线程池决定哪些线程须要回收时,只须要将其引用消除便可。Worker被建立出来后,就会不断地进行轮询,而后获取任务去执行,核心线程能够无限等待获取任务,非核心线程要限时获取任务。当Worker没法获取到任务,也就是获取的任务为空时,循环会结束,Worker会主动消除自身在线程池内的引用。
try { while (task != null || (task = getTask()) != null) { //执行任务 } } finally { processWorkerExit(w, completedAbruptly);//获取不到任务时,主动回收本身 }
线程回收的工做是在processWorkerExit方法完成的。
事实上,在这个方法中,将线程引用移出线程池就已经结束了线程销毁的部分。但因为引发线程销毁的可能性有不少,线程池还要判断是什么引起了此次销毁,是否要改变线程池的现阶段状态,是否要根据新状态,从新分配线程。
2.4.4 Worker线程执行任务
在Worker类中的run方法调用了runWorker方法来执行任务,runWorker方法的执行过程以下:
1.while循环不断地经过getTask()方法获取任务。
2.getTask()方法从阻塞队列中取任务。
3.若是线程池正在中止,那么要保证当前线程是中断状态,不然要保证当前线程不是中断状态。
4.执行任务。
5.若是getTask结果为null则跳出循环,执行processWorkerExit()方法,销毁线程。
执行流程以下图所示:
在当今的互联网业界,为了最大程度利用CPU的多核性能,并行运算的能力是不可或缺的。经过线程池管理线程获取并发性是一个很是基础的操做,让咱们来看两个典型的使用线程池获取并发性的场景。
场景1:快速响应用户请求
描述:用户发起的实时请求,服务追求响应时间。好比说用户要查看一个商品的信息,那么咱们须要将商品维度的一系列信息如商品的价格、优惠、库存、图片等等聚合起来,展现给用户。
分析:从用户体验角度看,这个结果响应的越快越好,若是一个页面半天都刷不出,用户可能就放弃查看这个商品了。而面向用户的功能聚合一般很是复杂,伴随着调用与调用之间的级联、多级级联等状况,业务开发同窗每每会选择使用线程池这种简单的方式,将调用封装成任务并行的执行,缩短整体响应时间。另外,使用线程池也是有考量的,这种场景最重要的就是获取最大的响应速度去知足用户,因此应该不设置队列去缓冲并发任务,调高corePoolSize和maxPoolSize去尽量创造多的线程快速执行任务。
场景2:快速处理批量任务
描述:离线的大量计算任务,须要快速执行。好比说,统计某个报表,须要计算出全国各个门店中有哪些商品有某种属性,用于后续营销策略的分析,那么咱们须要查询全国全部门店中的全部商品,而且记录具备某属性的商品,而后快速生成报表。
分析:这种场景须要执行大量的任务,咱们也会但愿任务执行的越快越好。这种状况下,也应该使用多线程策略,并行计算。但与响应速度优先的场景区别在于,这类场景任务量巨大,并不须要瞬时的完成,而是关注如何使用有限的资源,尽量在单位时间内处理更多的任务,也就是吞吐量优先的问题。因此应该设置队列去缓冲并发任务,调整合适的corePoolSize去设置处理任务的线程数。在这里,设置的线程数过多可能还会引起线程上下文切换频繁的问题,也会下降处理任务的速度,下降吞吐量。
线程池使用面临的核心的问题在于:线程池的参数并很差配置。一方面线程池的运行机制不是很好理解,配置合理须要强依赖开发人员的我的经验和知识;另外一方面,线程池执行的状况和任务类型相关性较大,IO密集型和CPU密集型的任务运行起来的状况差别很是大,这致使业界并无一些成熟的经验策略帮助开发人员参考。
关于线程池配置不合理引起的故障,公司内部有较多记录,下面举一些例子:
Case1:2018年XX页面展现接口大量调用降级:
事故描述:XX页面展现接口产生大量调用降级,数量级在几十到上百。
事故缘由:该服务展现接口内部逻辑使用线程池作并行计算,因为没有预估好调用的流量,致使最大核心数设置偏小,大量抛出RejectedExecutionException,触发接口降级条件,示意图以下:
Case2:2018年XX业务服务不可用S2级故障
事故描述:XX业务提供的服务执行时间过长,做为上游服务总体超时,大量下游服务调用失败。
事故缘由:该服务处理请求内部逻辑使用线程池作资源隔离,因为队列设置过长,最大线程数设置失效,致使请求数量增长时,大量任务堆积在队列中,任务执行时间过长,最终致使下游服务的大量调用超时失败。示意图以下:
业务中要使用线程池,而使用不当又会致使故障,那么咱们怎样才能更好地使用线程池呢?针对这个问题,咱们下面延展几个方向:
1. 可否不用线程池?
回到最初的问题,业务使用线程池是为了获取并发性,对于获取并发性,是否能够有什么其余的方案呢替代?咱们尝试进行了一些其余方案的调研:
综合考虑,这些新的方案都能在某种状况下提高并行任务的性能,然而本次重点解决的问题是如何更简易、更安全地得到的并发性。另外,Actor模型的应用实际上甚少,只在Scala中使用普遍,协程框架在Java中维护的也不成熟。这三者现阶段都不是足够的易用,也并不能解决业务上现阶段的问题。
2. 追求参数设置合理性?
有没有一种计算公式,可以让开发同窗很简易地计算出某种场景中的线程池应该是什么参数呢?
带着这样的疑问,咱们调研了业界的一些线程池参数配置方案:
调研了以上业界方案后,咱们并无得出通用的线程池计算方式。并发任务的执行状况和任务类型相关,IO密集型和CPU密集型的任务运行起来的状况差别很是大,但这种占比是较难合理预估的,这致使很难有一个简单有效的通用公式帮咱们直接计算出结果。
3. 线程池参数动态化?
尽管通过谨慎的评估,仍然不可以保证一次计算出来合适的参数,那么咱们是否能够将修改线程池参数的成本降下来,这样至少能够发生故障的时候能够快速调整从而缩短故障恢复的时间呢?基于这个思考,咱们是否能够将线程池的参数从代码中迁移到分布式配置中心上,实现线程池参数可动态配置和即时生效,线程池参数动态化先后的参数修改流程对好比下:
基于以上三个方向对比,咱们能够看出参数动态化方向简单有效。
3.3.1 总体设计
动态化线程池的核心设计包括如下三个方面:
3.3.2 功能架构
动态化线程池提供以下功能:
动态调参:支持线程池参数动态调整、界面化操做;包括修改线程池核心大小、最大核心大小、队列长度等;参数修改后及时生效。
任务监控:支持应用粒度、线程池粒度、任务粒度的Transaction监控;能够看到线程池的任务执行状况、最大任务执行时间、平均任务执行时间、95/99线等。
负载告警:线程池队列任务积压到必定值的时候会经过大象(美团内部通信工具)告知应用开发负责人;当线程池负载数达到必定阈值的时候会经过大象告知应用开发负责人。
操做监控:建立/修改和删除线程池都会通知到应用的开发负责人。
操做日志:能够查看线程池参数的修改记录,谁在何时修改了线程池参数、修改前的参数值是什么。
权限校验:只有应用开发负责人才可以修改应用的线程池参数。
参数动态化
JDK原生线程池ThreadPoolExecutor提供了以下几个public的setter方法,以下图所示:
JDK容许线程池使用方经过ThreadPoolExecutor的实例来动态设置线程池的核心策略,以setCorePoolSize为方法例,在运行期线程池使用方调用此方法设置corePoolSize以后,线程池会直接覆盖原来的corePoolSize值,而且基于当前值和原始值的比较结果采起不一样的处理策略。对于当前值小于当前工做线程数的状况,说明有多余的worker线程,此时会向当前idle的worker线程发起中断请求以实现回收,多余的worker在下次idel的时候也会被回收;对于当前值大于原始值且当前队列中有待执行任务,则线程池会建立新的worker线程来执行队列任务,setCorePoolSize具体流程以下:
线程池内部会处理好当前状态作到平滑修改,其余几个方法限于篇幅,这里不一一介绍。重点是基于这几个public方法,咱们只须要维护ThreadPoolExecutor的实例,而且在须要修改的时候拿到实例修改其参数便可。基于以上的思路,咱们实现了线程池参数的动态化、线程池参数在管理平台可配置可修改,其效果图以下图所示:
用户能够在管理平台上经过线程池的名字找到指定的线程池,而后对其参数进行修改,保存后会实时生效。目前支持的动态参数包括核心数、最大值、队列长度等。除此以外,在界面中,咱们还能看到用户能够配置是否开启告警、队列等待任务告警阈值、活跃度告警等等。关于监控和告警,咱们下面一节会对齐进行介绍。
线程池监控
除了参数动态化以外,为了更好地使用线程池,咱们须要对线程池的运行情况有感知,好比当前线程池的负载是怎么样的?分配的资源够不够用?任务的执行状况是怎么样的?是长任务仍是短任务?基于对这些问题的思考,动态化线程池提供了多个维度的监控和告警能力,包括:线程池活跃度、任务的执行Transaction(频率、耗时)、Reject异常、线程池内部统计信息等等,既能帮助用户从多个维度分析线程池的使用状况,又能在出现问题第一时间通知到用户,从而避免故障或加速故障恢复。
线程池负载关注的核心问题是:基于当前线程池参数分配的资源够不够。对于这个问题,咱们能够从事前和事中两个角度来看。事前,线程池定义了“活跃度”这个概念,来让用户在发生Reject异常以前可以感知线程池负载问题,线程池活跃度计算公式为:线程池活跃度 = activeCount/maximumPoolSize。这个公式表明当活跃线程数趋向于maximumPoolSize的时候,表明线程负载趋高。事中,也能够从两方面来看线程池的过载断定条件,一个是发生了Reject异常,一个是队列中有等待任务(支持定制阈值)。以上两种状况发生了都会触发告警,告警信息会经过大象推送给服务所关联的负责人。
在传统的线程池应用场景中,线程池中的任务执行状况对于用户来讲是透明的。好比在一个具体的业务场景中,业务开发申请了一个线程池同时用于执行两种任务,一个是发消息任务、一个是发短信任务,这两类任务实际执行的频率和时长对于用户来讲没有一个直观的感觉,极可能这两类任务不适合共享一个线程池,可是因为用户没法感知,所以也无从优化。动态化线程池内部实现了任务级别的埋点,且容许为不一样的业务任务指定具备业务含义的名称,线程池内部基于这个名称作Transaction打点,基于这个功能,用户能够看到线程池内部任务级别的执行状况,且区分业务,任务监控示意图以下图所示:
用户基于JDK原生线程池ThreadPoolExecutor提供的几个public的getter方法,能够读取到当前线程池的运行状态以及参数,以下图所示:
动态化线程池基于这几个接口封装了运行时状态实时查看的功能,用户基于这个功能能够了解线程池的实时状态,好比当前有多少个工做线程,执行了多少个任务,队列中等待的任务数等等。效果以下图所示:
面对业务中使用线程池遇到的实际问题,咱们曾回到支持并发性问题自己来思考有没有取代线程池的方案,也曾尝试着去追求线程池参数设置的合理性,但面对业界方案具体落地的复杂性、可维护性以及真实运行环境的不肯定性,咱们在前两个方向上可谓“举步维艰”。最终,咱们回到线程池参数动态化方向上探索,得出一个且能够解决业务问题的方案,虽然本质上仍是没有逃离使用线程池的范畴,可是在成本和收益之间,算是取得了一个很好的平衡。成本在于实现动态化以及监控成本不高,收益在于:在不颠覆原有线程池使用方式的基础之上,从下降线程池参数修改的成本以及多维度监控这两个方面下降了故障发生的几率。但愿本文提供的动态化线程池思路能对你们有帮助。
美团到店综合研发中心长期招聘前端、后端、数据仓库、机器学习/数据挖掘算法工程师,欢迎感兴趣的同窗发送简历到:tech@meituan.com(邮件标题注明:美团到店综合研发中心-上海)
阅读更多技术文章,请扫码关注微信公众号-美团技术团队!