Java线程池中的各个参数如何合理设置
⼀、前⾔
在开发过程中,好多场景要⽤到线程池。每次都是⾃⼰根据业务场景来设置线程池中的各个参数。
这两天⼜有需求碰到了,索性总结⼀下⽅便以后再遇到可以直接看着⽤。
虽说根据业务场景来设置各个参数的值,但有些万变不离其宗,掌握它的原理对如何⽤好线程池起了⾄关重要的作⽤。
那我们接下来就来进⾏线程池的分析。
⼆、ThreadPoolExecutor的重要参数
我们先来看下ThreadPoolExecutor的带的那些重要参数的构造器。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
...
}
1、corePoolSize: 核⼼线程数
这个应该是最重要的参数了,所以如何合理的设置它⼗分重要。
核⼼线程会⼀直存活,及时没有任务需要执⾏。
当线程数⼩于核⼼线程数时,即使有线程空闲,线程池也会优先创建新线程处理。
设置allowCoreThreadTimeout=true(默认false)时,核⼼线程会超时关闭。
如何设置好的前提我们要很清楚的知道CPU密集型和IO密集型的区别。
(1)、CPU密集型
CPU密集型也叫计算密集型,指的是系统的硬盘、内存性能相对CPU要好很多,此时,系统运作⼤部分的状况是CPU Loading 100%,CPU要读/写I/O(硬盘/内存),I/O在很短的时间就可以完成,⽽CPU还有许多运算要处理,CPU Loading 很⾼。
在多重程序系统中,⼤部分时间⽤来做计算、逻辑判断等CPU动作的程序称之CPU bound。例如⼀个计算圆周率⾄⼩数点⼀千位以下的程序,在执⾏的过程当中绝⼤部分时间⽤在三⾓函数和开根号的计算,便是属于CPU bound的程序。
CPU bound的程序⼀般⽽⾔CPU占⽤率相当⾼。这可能是因为任务本⾝不太需要访问I/O设备,也可能是因为程序是多线程实现因此屏蔽掉了等待I/O的时间。
(2)、IO密集型
IO密集型指的是系统的CPU性能相对硬盘、内存要好很多,此时,系统运作,⼤部分的状况是CPU在等I/O (硬盘/内存) 的读/写操作,此时CPU Loading并不⾼。
I/O bound的程序⼀般在达到性能极限时,CPU占⽤率仍然较低。这可能是因为任务本⾝需要⼤量I/O操作,⽽pipeline做得不是很好,没有充分利⽤处理器能⼒。
好了,了解完了以后我们就开搞了。
(3)、先看下机器的CPU核数,然后在设定具体参数:
⾃⼰测⼀下⾃⼰机器的核数
System.out.Runtime().availableProcessors());
即CPU核数 = Runtime().availableProcessors()
(4)、分析下线程池处理的程序是CPU密集型还是IO密集型
CPU密集型:corePoolSize = CPU核数 + 1
IO密集型:corePoolSize = CPU核数 * 2
2、maximumPoolSize:最⼤线程数
当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务。
当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务⽽抛出异常。
3、keepAliveTime:线程空闲时间
当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize。
如果allowCoreThreadTimeout=true,则会直到线程数量=0。
4、queueCapacity:任务队列容量(阻塞队列)
当核⼼线程数达到最⼤时,新任务会放在队列中排队等待执⾏
5、allowCoreThreadTimeout:允许核⼼线程超时
6、rejectedExecutionHandler:任务拒绝处理器
两种情况会拒绝处理任务:
当线程数已经达到maxPoolSize,且队列已满,会拒绝新任务。
当线程池被调⽤shutdown()后,会等待线程池⾥的任务执⾏完毕再shutdown。如果在调⽤shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务。
线程池会调⽤rejectedExecutionHandler来处理这个任务。如果没有设置默认是AbortPolicy,会抛出异常。ThreadPoolExecutor 采⽤了策略的设计模式来处理拒绝任务的⼏种场景。
这⼏种策略模式都实现了RejectedExecutionHandler 接⼝。
AbortPolicy 丢弃任务,抛运⾏时异常。
CallerRunsPolicy 执⾏任务。
DiscardPolicy 忽视,什么都不会发⽣。
DiscardOldestPolicy 从队列中踢出最先进⼊队列(最后⼀个执⾏)的任务。
三、如何设置参数
默认值:
corePoolSize = 1
maxPoolSize = Integer.MAX_VALUE
queueCapacity = Integer.MAX_VALUE
keepAliveTime = 60s
allowCoreThreadTimeout = false
rejectedExecutionHandler = AbortPolicy()
如何来设置呢?
需要根据⼏个值来决定
tasks :每秒的任务数,假设为500~1000
taskcost:每个任务花费时间,假设为0.1s
responsetime:系统允许容忍的最⼤响应时间,假设为1s
做⼏个计算
corePoolSize = 每秒需要多少个线程处理?
threadcount = tasks/(1/taskcost) = tasks*taskcout = (500 ~ 1000)*0.1 = 50~100 个线程。
corePoolSize设置应该⼤于50。
根据8020原则,如果80%的每秒任务数⼩于800,那么corePoolSize设置为80即可。
queueCapacity = (coreSizePool/taskcost)*responsetime
计算可得 queueCapacity = 80/0.1*1 = 800。意思是队列⾥的线程可以等待1s,超过了的需要新开线程来执⾏。
切记不能设置为Integer.MAX_VALUE,这样队列会很⼤,线程数只会保持在corePoolSize⼤⼩,当任务陡增时,不能新开线程来执⾏,响应时间会随之陡增。
maxPoolSize 最⼤线程数在⽣产环境上我们往往设置成corePoolSize⼀样,这样可以减少在处理过程中创建线程的开销。rejectedExecutionHandler:根据具体情况来决定,任务不重要可丢弃,任务重要则要利⽤⼀些缓冲机制来处理。keepAliveTime和allowCoreThreadTimeout采⽤默认通常能满⾜。
以上都是理想值,实际情况下要根据机器性能来决定。如果在未达到最⼤线程数的情况机器cpu load已经满了,则需要通过升级硬件和优化代码,降低taskcost来处理。
以下是我⾃⼰的的线程池配置:
@Configuration
public class ConcurrentThreadGlobalConfig {
@Bean
public ThreadPoolTaskExecutor defaultThreadPool() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//核⼼线程数⽬
executor.setCorePoolSize(65);
//指定最⼤线程数
executor.setMaxPoolSize(65);
//队列中最⼤的数⽬
executor.setQueueCapacity(650);
/
/线程名称前缀
executor.setThreadNamePrefix("DefaultThreadPool_");
//rejection-policy:当pool已经达到max size的时候,如何处理新任务
//CALLER_RUNS:不在新线程中执⾏任务,⽽是由调⽤者所在的线程来执⾏
//对拒绝task的处理策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//线程空闲后的最⼤存活时间
executor.setKeepAliveSeconds(60);
//加载
executor.initialize();
return executor;
java线程池创建的四种
}
}
四、线程池队列的选择
workQueue - 当线程数⽬超过核⼼线程数时⽤于保存任务的队列。主要有3种类型的BlockingQueue可供选择:⽆界队列,有界队列和同步移交。从参数中可以看到,此队列仅保存实现Runnable接⼝的任务。
这⾥再重复⼀下新任务进⼊时线程池的执⾏策略:
当正在运⾏的线程⼩于corePoolSize,线程池会创建新的线程。
当⼤于corePoolSize⽽任务队列未满时,就会将整个任务塞⼊队列。
当⼤于corePoolSize⽽且任务队列满时,并且⼩于maximumPoolSize时,就会创建新额线程执⾏任务。
当⼤于maximumPoolSize时,会根据handler策略处理线程。
1、⽆界队列
队列⼤⼩⽆限制,常⽤的为⽆界的LinkedBlockingQueue,使⽤该队列作为阻塞队列时要尤其当⼼,当任务耗时较长时可能会导致⼤量新任务在队列中堆积最终导致OOM。
阅读代码发现,wFixedThreadPool 采⽤就是 LinkedBlockingQueue,⽽博主踩到的就是这个坑,当QPS很⾼,发送数据很⼤,⼤量的任务被添加到这个⽆界LinkedBlockingQueue 中,导致cpu和内存飙升服务器挂掉。
当然这种队列,maximumPoolSize 的值也就⽆效了。
当每个任务完全独⽴于其他任务,即任务执⾏互不影响时,适合于使⽤⽆界队列;例如,在 Web 页服务器中。
这种排队可⽤于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许⽆界线程具有增长的可能
性。
2、有界队列
当使⽤有限的 maximumPoolSizes 时,有界队列有助于防⽌资源耗尽,但是可能较难调整和控制。
常⽤的有两类,⼀类是遵循FIFO原则的队列如ArrayBlockingQueue,另⼀类是优先级队列如PriorityBlockingQueue。
PriorityBlockingQueue中的优先级由任务的Comparator决定。
使⽤有界队列时队列⼤⼩需和线程池⼤⼩互相配合,线程池较⼩有界队列较⼤时可减少内存消耗,降低cpu使⽤率和上下⽂切换,但是可能会限制系统吞吐量。
3、同步移交队列
如果不希望任务在队列中等待⽽是希望将任务直接移交给⼯作线程,可使⽤SynchronousQueue作为等待队列。
SynchronousQueue不是⼀个真正的队列,⽽是⼀种线程之间移交的机制。要将⼀个元素放⼊SynchronousQueue中,必须有另⼀个线程正在等待接收这个元素。
只有在使⽤⽆界线程池或者有饱和策略时才建议使⽤该队列。
以上为个⼈经验,希望能给⼤家⼀个参考,也希望⼤家多多⽀持。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论