✅如何进行线程池调优?
典型回答
Java的线程池调优是一个比较常见的问题,但是!虽然面试常见,但是其实工作中并不经常做(大部分公司都不太需要调优。。。
想知道怎么做线程池调优,首先要知道,线程池都能调哪些东西。
上面这篇中介绍过,线程池的一些核心参数,其实调优就是围绕着这些参数来做的。
- corePoolSize: 核心线程数量,可以类比正式员工数量,常驻线程数量。
- maximumPoolSize: 最大的线程数量,公司最多雇佣员工数量。常驻+临时线程数量。
- workQueue:多余任务等待队列,再多的人都处理不过来了,需要等着,在这个地方等。
- keepAliveTime:非核心线程空闲时间,就是外包人员等了多久,如果还没有活干,解雇了。
- threadFactory: 创建线程的工厂,在这个地方可以统一处理创建的线程的属性。每个公司对员工的要求不一样,恩,在这里设置员工的属性。
- handler:线程池拒绝策略,什么意思呢?就是当任务实在是太多,人也不够,需求池也排满了,还有任务咋办?默认是不处理,抛出异常告诉任务提交者,我这忙不过来了。
线程数
线程池有两个重要的和线程数有关的参数,就是corePoolSize和maximumPoolSize,一般线程池调优的时候,大部分也都是调这两个参数。。
上面这篇中我们介绍过了,可以根据我们给过的公式,先设置一个核心线程数的初始值。简单的的话就是:
- 如果是CPU密集型应用,则核心线程数可以设置为N+1
- 如果是IO密集型应用,则核心线程数可以设置为2N+1,或者是2N-4N之间也都是可以的。
也就是说,IO密集型的应用,你的线程数可以设置的更多一些,因为对于I/O密集型任务,等待时间通常远大于计算时间,这意味着可以分配更多的线程。当一个线程在等待时(如等待网络响应),CPU可以切换到另一个线程进行计算,从而提高CPU利用率。
那如果想要一个更加准确一点的公式,那么可以用这个:
$ 线程数=CPU核心数×目标CPU利用率×\frac{(1+等待时间/计算时间)}{1}
$
设置了初始的线程数之后,就可以根据线上的运行情况,或者直接做压测的情况,来看线程数是不是需要调整,怎么看呢?主要观察这几个指标(这些指标都可以通过prometheus来实现监控,这里不展开了,在我的项目课中有监控的实践。):
- 活动线程数:当前正在执行任务的线程数。通过它可以了解当前线程池的负载。如果这个值长期处于较高水平,可能意味着需要更多线程来处理任务。
- **队列长度 **:阻塞队列的长度,代表等待执行的任务数。如果队列长度经常很长,说明任务的提交速度超过了线程池的处理能力。
- 已完成任务数:表示已处理完成的任务数,帮助你评估线程池处理任务的速度。
- **任务拒绝数 **:如果任务数超过了线程池能处理的最大负载,拒绝策略会启动。如果拒绝的任务数较多,说明线程池容量不足,可能需要增加线程数或调整队列长度。
- CPU利用率:表示CPU正在处理任务的时间占总时间的比例。通常,高 CPU 利用率意味着系统正在高效地使用计算资源,而低 CPU 利用率可能意味着计算资源没有得到充分利用,可能会有性能瓶颈。
- 线程上下文切换次数:操作系统切换 CPU 上运行的线程的过程。每当线程状态发生变化(比如从运行状态变为等待状态,或从等待状态变为运行状态),操作系统就会进行一次上下文切换。
一般会出现以下集中现象:
1、队列长度长且任务完成速度慢,说明线程池处理能力不足。可以考虑增加核心线程数或最大线程数,或者增大队列容量。
2、频繁的线程上下文切换,意味着系统在管理线程时花费了大量的 CPU 时间,这通常是由于线程数过多导致的。特别是在CPU 密集型任务中,线程数过多会导致CPU资源被上下文切换消耗,反而影响任务的执行效率。这时候要考虑降低线程数。
3、高CPU利用率,说明大部分 CPU 时间都在执行任务,通常表示系统在高负载运行。可以适当的降低线程数。
4、任务被拒绝的数量较多,说明当前线程池无法处理当前的负载。需要通过增加线程数、增加队列容量,或优化任务处理速度来解决。
对于最大线程数,一般就设置为核心线程数的2倍就行了,或者有的也可以直接设置成和核心线程数一样。
所以,核心线程数的调节可以遵守这个原则(个人总结,如有雷同,纯属缘分):
- 提升线程数:线程数不够了(队列长、任务执行慢、任务被拒绝), 并且提高也不会对系统造成影响(CPU利用率不高,load不高)的情况下。
- 降低线程数:用不上这么多线程(活跃线程数低)或者线程多反而导致执行慢(上下文切换多)或者线程数对系统造成影响了(CPU利用率高,load高)。
队列
线程池中的任务队列用于存放待处理的任务,直到有空闲线程来执行它们。常用的队列类型有:
- ArrayBlockingQueue:基于数组的有界队列,适用于任务数已知的情况。
- LinkedBlockingQueue:基于链表的有界或无界队列,适用于任务数不确定的情况。
- SynchronousQueue:一个没有内部容量的队列,每个插入操作必须等待一个相应的移除操作,适用于任务量大且短时间内高并发的情况。
- PriorityBlockingQueue:优先级队列,适用于任务要按照优先级执行的情况。
对于负载较高的场景,可以使用 LinkedBlockingQueue 或 SynchronousQueue,它们能够适应动态的任务需求。而对于任务较为平稳且队列大小可以预测的场景,可以使用 ArrayBlockingQueue。
拒绝策略
当线程池中的线程数已达最大限制且队列也满了,就会执行拒绝策略。有以下几种:
- AbortPolicy(默认):抛出
RejectedExecutionException。 - CallerRunsPolicy:由调用者线程执行任务。
- DiscardPolicy:直接丢弃任务。
- DiscardOldestPolicy:丢弃队列中最旧的任务。
一般来说默认的就行了,但是如果任务非常重要,推荐使用 CallerRunsPolicy,确保任务不会丢失。