线程池的思考

到底该如何设计与使用线程池?

线程成本

线程的创建与消耗释有成本的,大体可以分为:CPU资源的消耗内存资源的消耗

CPU资源的消耗

CPU资源的消耗是指大量的线程会对CPU进行竞争抢占,大量的线程上下文切换会导致CPU的吞吐量降低

那么,该如何设置线程数呢?

Using profiling, you can estimate the ratio of waiting time (WT) to service time (ST) for a typical request.
If we call this ratio WT/ST, for an N-processor system, you’ll want to have approximately N*(1+WT/ST) threads
to keep the processors fully utilized. 来源

假设等待时间为WT,实际运行时间为ST,CPU核心数为N,则最佳线程数Num为:

Num = N * (1 + WT / ST)

考虑两种CPU运算:IO密集型CPU密集型


对于IO密集型来说,线程实际执行的时间很短,假设线程执行时间与线程等待时间近似,

此时,WT/ST≈1Num=N*(1+1)=2*N


对于CPU密集型来说,线程实际的执行时间很长,会远大于线程等待时间,

此时,WT/ST≈0Num=N*(1+0)=N

但由于线程切换是基于等待线程的,因此需要增加一个等待线程来避免新线程创建导致的CPU空闲,

因此,Num = N + 1

上面的计算都是基于CPU充分使用的前提下,那么如果保证CPU性能的冗余来估计线程数量呢?

Num = N * U * (1 + WT / ST),其中,U为CPU的目标使用率。

内存资源的消耗

首先,线程的申请包括线程资源的申请线程资源的初始化以及线程资源的释放

Java中线程独占的资源有虚拟机栈本地方法栈计数器

除了线程本身的内存占用,线程执行的任务也会占用大量的内存资源。

线程任务占用的内存与具体的业务逻辑有关,如果使用大量的并发线程必然导致内存资源的不足。


例如,一个报表系统,通过接收MQ消息,定时触发报表的生成与上传,

其中,MQ消息接受后会把报表生成与上传的任务放到线程池中异步执行,

但由于近期数据量翻倍增长,导致报表数据翻倍,从而每个任务占用的内存也翻倍了,

在使用原有的线程池时,同样的线程数会占用翻倍的内存,从而产生内存不足的情况。

线程执行由于需要资源的申请、初始化,一定会带有一定的时延。

线程池

为什么需要使用线程池?

  • 线程池降低资源(CPU与内存)消耗;
  • 线程池提高任务响应速度;
  • 线程池提供线程的可管理性;
  • 线程池提供扩展能力(任务拒绝策略)。

核心原理

线程池的核心原理是线程资源复用线程资源的隔离与管理

线程资源复用

线程池通过保证一定数量存活的线程来实现资源复用。

线程复用的优点:

① 避免资源的频繁申请与释放,提高CPU与内存的使用效率;

② 避免资源申请的时延,提高任务响应能力。

线程资源隔离与管理

线程池通过限制存活线程的数量以及任务队列来实现资源的隔离与管理。

资源的隔离是指不同的业务场景使用不同的线程池,从而实现线程资源的隔离。

资源的管理是指线程池可以控制线程的数量、缓存任务的数量以及任务执行的顺序。

资源隔离与管理的优点:

① 避免无限的线程申请导致资源耗尽的情况;

② 协调任务线程与工作线程对CPU资源的竞争,避免大量任务线程对工作线程的资源(CPU与Memory)抢占。

关键属性

核心线程数:默认存活的线程数量CoreSize

最大线程数:最大存活的线程数量MaxPoolSize

空闲线程存活时间:核心线程之外的空闲线程最长存活时间KeepliveTime

以上三个参数是用来设置线程池中存活线程的数量,是影响线程池的吞吐量以及任务执行时延的关键。

任务队列:队列类型与大小WorkQueue(n)

任务拒绝策略:当线程池无法及时响应任务时的执行策略(仅配置有界队列才会触发)。

以上两个参数是用来设置线程池可以缓存任务的数量以及过多任务的拒绝策略,是影响线程池的最大响应能力以及降级策略的关键。

实现原理

线程池的实现原理如图所示,

线程调度策略

线程池的调度是基于任务队列实现的。

基于任务队列,线程池实现了任务与线程解耦。
任务队列负责任务的存储以及分配,工作线程负责任务的执行。

任务投递到线程池会优先加入任务队列,工作线程会从任务队列竞争获取待处理的任务。

具体调度规则如下:

  • WorkerCount < CoreSize : 创建新线程
  • WorkerCount >= CoreSize : 任务添加到阻塞队列
  • WorkerCount >= CoreSize & 阻塞队列已满 : 创建新的线程
  • WorkerCount >= CoreSize & 阻塞队列已满 & WorkerCount >= MaxPoolSize : 执行任务拒绝策略

任务分配策略

任务分配模式是基于任务队列拒绝策略组成。

任务队列可以分为三类:有界队列无界队列同步移交队列

有界队列是指队列大小有限,缓存任务的数量存在上限,当缓存数量达到上限时会触发拒绝策略

无界队列是指队列大小无限,如果线程池的吞吐量不足会导致缓存任务不断增多,由于无界队列不会触发拒绝策略,因此,可能导致内存不足

同步队列是指任务直接交给工作线程,不存在任务缓存的情况,提交任务会阻塞至空闲线程的认领

无界队列:LinkedBlockingQueue、DelayedWorkQueue
有界队列:ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue
同步队列:SynchronousQueue

具体实现

newFixedThreadPool

使用LinkedBlockingQueue作为无界队列(存在OOM的可能),线程数量固定,因此,此线程池的吞吐量有限。

newCachedThreadPool

使用SynchronousQueue作为同步队列,虽然不存在无限增大的任务队列,但是存在无限增加的线程,因此,此线程池的线程数量不可控。

newScheduledThreadPool

使用DelayedWorkQueue作为无界队列,线程数量无限,任务数量与线程数量均不可控,因此,此线程池也存在很大的安全隐患。

阿里巴巴开发者手册规范:

线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,避免资源耗尽的风险。

衡量标准

线程分类

线程一般可以分为两种类型:IO密集型计算密集型

IO密集型不会占用大量的CPU资源,大部分线程处于IO阻塞的情况,因此可以使用更多的线程来处理任务。

计算密集型会占用大量的CPU资源,线程切换只会影响计算效率,因此不建议使用大量的并发线程。

CPU与内存使用率

大量的计算密集型线程会导致CPU的抢占,会降低CPU的使用效率,导致更大的平均时延。

大量的IO密集型线程由于IO阻塞会占用更多的内存资源,会降低内存的使用效率,可能造成OOM。

吞吐量

吞吐量是指线程池在单位时间内可以执行的任务数量。

工作线程是影响任务并发量的关键因素,也就是如何设计CoreSizeMaxCoreSize是影响线程池吞吐量的关键。

此外,业务任务往往会占用大量的内存资源,从而也需要考虑是否存在核心线程数过多导致的资源浪费

使用场景

并行资源请求

如果一个业务请求逻辑会涉及多个资源调用,可以使用线程池来并行请求多个资源。

虽然这种方式一定会降低资源请求的时延,但也会导致线程数量的倍数增长。

队列的选择

一般建议使用有界队列,由于可能触发任务丢弃策略,因此需要自定义实现降级策略。

线程数该如何选择?

吞吐量的计算公式为:TPS = 完成任务数量 / 完成的时间

大量高并发任务请求会造成线程池吞吐量不足的情况,需要考虑降级策略。

增大线程数量可以提高任务并行能力,从而提供线程池的吞吐量,这种用法其实是很危险的,属于空间换时间的一种策略。

也存在资源浪费的问题,由于前面集中资源出现问题,导致后面的资源无需加载的情况。

是否采取这样的方式与具体的业务场景有关,线程池是否可控,是否需要考虑降级策略等等。

并行批处理任务

数据合聚合与处理任务,由于数据量的不断增大,原有顺序执行的方式所需要的时间成倍增加,因此,需要拆分为多个子任务并行执行。

后台任务一般属于CPU密集型,线程数应该与CPU数量一致,过多线程只会增加CPU的抢占。

并行任务可能会占用多倍的内存,因此需要仔细计算内存占用情况是否会导致机器内存不足。

降级策略

线程池只是一种资源集中管理与优化的手段,它并不能解决资源不足与资源竞争的问题。

在线程池无法支持当前任务的情况下,需要提前设计适当的降级策略。

线程池的降级策略实际可以理解为线程池的拒绝策略

那么如何设计线程池的拒绝策略?

① 动态调整线程池配置,线程池的配置应该与具体的业务场景有关。

在高并发的场景下,动态调整线程池的配置对机器CPU、内存会造成不同情况的影响,严重情况下导致系统不可用。

② 异常报警,是异常监控的必要手段。

③ 任务持久化到DB、MQ,利用异步线程、延迟消息等方式再次触发。

参考

Java线程池实现原理及其在美团业务中的实践
如何合理地估算线程池大小?
Java线程池-ThreadPoolExecutor原理分析
Thread pools and work queues
Thread Pools in Java
Oracle ThreadPoolExecutor