java线程池参数自定义设置详解
作者:zhongh Jim
引言
上一篇线程池+ FutureTask异步执行多任务只介绍了怎么搭配使用线程池,但没有说明里面的线程池的参数是怎么设置的,那么本文就说明一下。
这里把上篇文章的线程池参数设置贴出来:
//给这个接口的线程池定义里边的线程名字 ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("thread-start-runner-%d").build(); ExecutorService taskExe= new ThreadPoolExecutor(10,20,800L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>(100),namedThreadFactory);
这些参数也不都是随意设置的,而是有一定的考量思路,下面会一 一介绍
先介绍一下线程池的构造函数
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { ... }
我们创建线程池一般是手动设置线程池的参数,已经不建议使用Executors的FixedThreadPool 、SingleThreadPool、CachedThreadPool了
因为:
- FixedThreadPool 、SingleThreadPool会的任务等待队列均为
new LinkedBlockingQueue<Runnable>()
,允许的队列长度为 Integer.MAX_VALUE,存在任务堆积导致OOM内存溢出的隐患 - 而CachedThreadPool允许的最大线程数量为
Integer.MAX_VALUE
,而且核心线程数为0,意味着 只要有任务进来,就会频繁创建新线程,没有任务之后又要关闭线程,耗费性能。另一方面,由于允许创建大量的线程,也有导致OOM的潜在隐患
设置线程池参数需要参考几个数值:
tasks
:每秒任务数,运维反馈是平均每秒 38个
taskcost
:每个任务花费时间,0.2s
(3) responsetime
:系统容忍(线程等待最长时间)的最大时间1s
corePoolSize:核心线程数
核心线程会一直存活,不管空不空闲,但如果设置了setAllowCoreThreadTimeout(true)
会让核心线程在空闲超时后关闭
计算方式:corePoolSize=tasks/(1/taskcost) =tasks * taskcost =38*0.2=7.6 个
查阅了下文章,大佬说计算密集型(遍历+判断的逻辑耗时占比多)的接口可将核心线程设置为:
corePoolSize=CPU核数+1 =8+1=9,设置为10就好了
如何查看CPU核数:
System.out.println(Runtime.getRuntime().availableProcessors());
设置得稍微大一点,也能减少频繁创建额外线程带来的开销
maxPoolSize:最大线程数
如果核心线程数不够用,会创建额外的线程来执行任务。
创建额外线程的条件(缺一不可):
- 现有的线程数< 最大线程数maxPoolSize and 现有线程数 > corePoolSize核心线程数
- 任务队列填满了
最大线程数我们设置的相对随意了些, 令maxPoolSize= 2* corePoolSize=20,大概能应对突然暴增的业务查询请求
keepAliveTime额外线程的可空闲时间
额外线程就是在核心线程数的基础上 另外创建的线程
额外线程空闲了keepAliveTime的时间后,线程退出,直至现有的线程数量=corePoolSize核心线程数
TimeUnit.MILLISECONDS
是毫秒单位
workQueue任务队列
常见的有3种:
(1) 无限队列LinkedBlockingQueue()
构造函数是new LinkedBlockingQueue<Runnable>()
允许的任务等待队列的最大长度为:Integer.MAX_VALUE,即能无限的接收新的任务,任何的拒绝策略也差不多没有意义了。
另外,maximumPoolSize
这个参数也没有意义了,因为只有同时满足 核心线程数量够了 + 任务队列workQueue满了 + 现有的线程数<maximumPoolSize最大线程数,才会去创建额外的线程
- 好处是LinkedBlockingQueue在应对突然暴增的请求时,它不会抛异常拒绝
- 缺点是任务堆积过度没有及时处理的话,容易导致内存溢出
那咱们就不用这个队列了吧
(2) 有界队列
new LinkedBlockingQueue(int capacity)
:固定容量的阻塞队列new ArrayBlockingQueue<Integer>(int capacity,true);
其中true是公平锁,只能FIFO排队一 一执行;false允许任务插队,会存在晚来的任务先执行的情况PriorityBlockingQueue(int initialCapacity, Comparator<? super E> comparator)
:默认会创建长度为11的优先级队列,第二个参数comparator会按照我们指定的方式进行排序
我们的任务基本的执行顺序基本也是先进先出,直接用了new LinkedBlockingQueue(int capacity),把容量设置得大一点,那样就不会轻易的填满队列导致频繁地创建额外的线程,减少线程频繁切换
(3) SynchronousQueue
new SynchronousQueue()
:队列长度为0,要添加新任务必须得有空闲的线程才能添加,因此要求 maximumPoolSize尽可能的大,还得 配置拒绝策略
最终, 我们选择了new LinkedBlockingQueue(int capacity)
作为任务队列
任务队列的长度
queueCapacity = (coreSize/taskcost) * responsetime=8/0.2*1=80
,队列长度设置为100也可
RejectedExecutionHandler拒绝策略
- AbortPolicy:抛异常
- DiscardPolicy:丢任务
- DiscardOldestPolicy:将队列头部的任务丢了,也就是把最早进入队列等待的任务丢了
- CallerRunsPolicy:将新任务(皮球)踢回给主线程执行,让主线程在接下来的时间能无法提交新任务,典型的踢皮球策略
我们选择了默认的AbortPolicy抛异常:
抛异常的话,需要上游系统截获异常,并告知用户请求繁忙稍等一下
如果是DiscardPolicy丢任务的话我猜大概率是用户得不到响应吧,没这么搞过
线程工厂
它还是很有必要设置的,因为系统的线程池不止一个,不设置一下线程工厂,不给线程定义个名字的话,很难看到是哪个线程池的线程在跑,因为线程的名字都被写死成pool-1-thread-1
、pool-1-thread-2
、pool-2-thread-1
…
那么,问题来了:如何判断线程池里边的指定线程是否在执行任务?
更多关于线程池参数自定义的资料请关注脚本之家其它相关文章!