我们创建线程池一般是手动设置线程池的参数,已经不建议使用Executors的FixedThreadPool 、SingleThreadPool、CachedThreadPool了
为什么我们不推荐使用 ?
这是因为:
(1)FixedThreadPool 、SingleThreadPool会的任务等待队列均为new LinkedBlockingQueue(),允许的队列长度为 Integer.MAX_VALUE,存在任务堆积导致OOM内存溢出的隐患
(2)而CachedThreadPool允许的最大线程数量为Integer.MAX_VALUE,而且核心线程数为0,意味着 只要有任务进来,就会频繁创建新线程,没有任务之后又要关闭线程,耗费性能。另一方面,由于允许创建大量的线程,也有导致OOM的潜在隐患
线程池中有多个关键参数,需要在创建线程池时对其进行设置,合理的参数设置能够达到最佳的性能,适应任务场景。这里以ThreadPoolExecutor为例,对几个重要的参数进行解析说明:
1.corePoolSize:核心线程数
线程池中一直保持存活的线程数量,即使它们处于空闲状态。
如果线程池中的线程数小于核心线程数,新的任务将创建新线程来处理,即使有核心线程处于空闲状态。
这是为了更快地响应新任务而采取的策略,确保核心线程一直保持存活状态。
补充:但如果设置了setAllowCoreThreadTimeout(true)会让核心线程在空闲超时后关闭。
A:设置线程池参数需要参考几个数值:
tasks:每秒任务数,运维反馈是平均每秒 38个
taskcost:每个任务花费时间,0.2s
responsetime:系统容忍(线程等待最长时间)的最大时间1s
B:在配置corePoolSize之前,需要先了解什么是CPU密集型任务和I/O密集型任务
**CPU密集型任务(遍历+判断的逻辑耗时占比多):**需要大量使用CPU计算资源的任务。这些任务通常需要进行复杂的数值计算、图像处理、模拟仿真等操作,对CPU的计算能力有很高的要求。相对于I/O密集型任务,CPU密集型任务更加依赖CPU的处理能力,而不是I/O速度。
**I/O密集型任务:**需要大量使用I/O(输入/输出)操作的任务。这些任务通常需要频繁地读写磁盘、网络或其他外部设备,如文件处理,数据库操作,网络传输等,这些操作对I/O速度有很高的要求。相对于CPU密集型任务,I/O密集型任务更加依赖I/O速度,而不是CPU计算能力。
参数配置:
CPU密集型:corePoolSize = CPU核数 + 1
如何查看CPU核数:
System.out.println(Runtime.getRuntime().availableProcessors());
corePoolSize=CPU核数+1 =8+1=9,设置为10就好了,设置得稍微大一点,也能减少频繁创建额外线程带来的开销。
IO密集型:corePoolSize = CPU核数 * 2
2.maximumPoolSize:最大线程数
线程池所允许的最大线程个数
如果核心线程数不够用,会创建额外的线程来执行任务,创建额外线程的条件(缺一不可):
(1)现有的线程数< 最大线程数maxPoolSize and 现有线程数 > corePoolSize核心线程数。
(2)任务队列填满了.
最大线程数一般设置为CPU核数*2。
注意:当线程数=maxPoolSize,且任务队列已满时,线程池会根据handle策略处理,默认是AbortPolicy 丢弃任务,抛运行时异常。
maxPoolSize往往设置成corePoolSize一样,这样可以减少在处理过程中创建线程的开销。
3.keepAliveTime:线程存活时间
额外线程就是在核心线程数的基础上 另外创建的线程
额外线程空闲了keepAliveTime的时间后,线程退出,直至现有的线程数量=corePoolSize核心线程数
注意:如果allowCoreThreadTimeout=true,则会直到线程数量=0
4.workQueue工作队列
一般选择建议选择有界队列,因为如果任务特别多,核心线程处理不过来,会将任务都放到工作队列中,此时最大线程数已经没有意义了。如果控制不好会导致OOM。
LikendBlockingQueue需要维护一个个Node对象,需要额外的内存消耗。并且在生产和消费的时候,需要创建Node对象进行插入或移除,大批量数据的系统中,其对于GC的压力会比较大。
ArrayBlockingQueue只是维护final Object[] items;一个数组。在生产和消费的时候,是按照索引对数据插入或移除的,不会产生或销毁任何额外的对象实例。
综合来说,可以选择ArrayBlockingQueue
更详细的队列解析可见博文:
5.queueCapacity:队列容量
当核心线程数达到最大时,新任务会放在队列中排队等待执行。
taskcost:任务耗时
responsetime:相应时间
queueCapacity = (coreSizePool/taskcost)*responsetime
6.rejectedExecutionHandler:拒绝策略
两种情况会拒绝处理任务:
当线程数已经达到maxPoolSize,且队列已满,会拒绝新任务。
当线程池被调用shutdown()后,会等待线程池里的任务执行完毕再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务。线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置默认是AbortPolicy,会抛出异常。
JDK提供了四种拒绝策略:
AbortPolicy:直接丢弃新任务,抛出异常
DiscardPolicy:直接丢弃掉,不会抛出异常
DiscardOldestPolicy:丢弃时间最久的任务。一般是队列最前面的任务 (将队列头部的任务丢了,也就是把最早进入队列等待的任务丢了)
CallerRunsPolicy:交给主线程去执行
参数设置原理:
为了最大程度利用线程池的资源,充分发挥线程池的执行效率,需要对线程池的主要参数进行合理的设置,对于不同的业务和场景,也需要根据实际情况来进行调整。
核心线程池大小corePoolSize和最大线程池大小maximumPoolSize一般需要根据实际场景设置,主要与执行任务的类型和数量相关。一般最佳实践建议是将核心线程池设置为CPU核心数 + 1,最大线程池大小设置为CPU核心数 x 2。
KeepAliveTime线程存活时间,一般根据任务处理的耗时配置。如果任务密集且耗时长,则可以适当增加空闲线程的存活时间,根本目的是尽可能减少线程的创建和销毁操作,原则上不超过60s。
workQueue阻塞队列的类型及大小需要根据具体场景来设置。通常来讲任务数量多或并发高,选择无界队列,避免任务被拒绝。任务数量可控选择有界队列
参考链接:
https://www.jb51.net/program/304855xjh.htm
https://blog.csdn.net/weixin_50591390/article/details/136032868