线程池的几个灵魂拷问(二)
线程池虽然在并发编程里很强大,但线程池使用面临的核心的问题在于:线程池的参数并不好配置。一方面线程池的运行机制不是很好理解,配置合理需要强依赖开发人员的个人经验和知识;另一方面,线程池执行的情况和任务类型相关性较大,IO密集型和CPU密集型的任务运行起来的情况差异非常大,这导致业界并没有一些成熟的经验策略帮助开发人员参考。
美团方案
比如网上流传的比较多的一个策略:
- 如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为
N(CPU)+1
(比如是4核心 就配置为5) - 如果是IO密集型任务,参考值可以设置为
2*N(CPU)
CPU密集型的为什么要+1呢?《Java并发编程实战》给出的原因是:即使当计算(CPU)密集型的线程偶尔由于页缺失故障或者其他原因而暂停时,这个“额外”的线程也能确保 CPU 的时钟周期不会被浪费。
这里先来看看美团帮我们总结的现在业界的一些线程池调参方案:
cpu参数方案第一套方案是并发编程实战给出的,明显太理论化了,和实际业务想去甚远!
N(threads) = N(Cpu个数)*U(cpu的使用率)*(1+ 等待时间/计算时间)
第二套方案就没有考虑多个业务线程池的情况。
第三套方案的用到了TPS来参与计算,但是这也是流量恒定情况下算出来的,真实情况往往比较随机。
有啥比较好的办法吗?——那就是:线程池参数动态化,采用这种方案最好就是用这么一个办法来做:
- 简化线程池配置:线程池构造参数有8个,但是最核心的是3个:corePoolSize、maximumPoolSize,workQueue,它们最大程度地决定了线程池的任务分配和线程分配策略
- 参数可动态修改:为了解决参数不好配,修改参数成本高等问题
- 加线程池监控
为什么能做到动态修改线程池参数呢?这是因为JDK本身就提供api方法支持动态的修改:
设置核心线程数的大小至于如何在运行时状态实时查看,这里也有一个办法:用户基于JDK原生线程池ThreadPoolExecutor提供的几个public的getter方法,可以读取到当前线程池的运行状态以及参数:
线程池的运行时状态用户基于这个功能可以了解线程池的实时状态,比如当前有多少个工作线程,执行了多少个任务,队列中等待的任务数等等。
Netty进阶指南给出来的方案
在Netty服务编写的过程中,也要涉及到两个线程池的参数配置,尤其是IO线程池的配置,这里书中也给了一套经验方案来针对线程的监控情况,可以参考:
同样的先用CPU核数*2,看看是否存在瓶颈,运行时的监控则用比较土的办法了:
- 打印thread dump,同时获取当时cpu排在前面几个的线程号
- 然后在线程dump文件中去对应的线程号堆栈
- 然后在堆栈中查找是否有SelectotImpl.lookAndDoSelect处的lock信息
如果多次采集都发现有这堆信息的话,说明此时此刻的IO线程比较空闲,无需调整;但是如果一直在read或者write的执行处,则说明IO较为繁忙,可以适当的去调大NioEventLoop线程的个数来提升网络的读写性能。但是这边线程数的改动就不是动态化的了,服务启动后指定的线程数就不能再修改了。
参考文章
1、Java线程池实现原理及其在美团业务中的实践
2、微信文章:如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答。