Java线程池( 四 )


确定线程池的大小线程池合理的长度取决于所要执行的任务特征以及程序所部署的系统环境 , 一般根据这二者因素使用配置文件提供或者通过CPU核数N:
N = Runtime.getRuntime().availableProcessors();
动态计算而不是硬编码在代码中 。主要是避免线程池过大或过小这两种极端情况 。如果线程池过大 , 会导致CPU和内存资源竞争 , 频繁的上下文切换 , 任务延迟 , 甚至资源耗尽 。如果线程池过小 , 会造成CPU和内存资源未充分利用 , 任务处理的吞吐量减小 。
对于任务特征来说 , 需要分清楚是计算密集型任务 , 还是IO密集型任务或是混合型任务 。

计算密集型也称为CPU密集型 , 意思就是该任务需要大量运算 , 而没有阻塞 , CPU一直全速运行 。CPU密集型任务只有在多核CPU上才可能得到加速 , 即scale up , 通过多线程程序享受到增加CPU核数带来的好处 。
IO密集型 , 即该任务需要大量的IO操作 , 例如网络连接、数据库连接等等 , 执行任务过程中会有大量的阻塞 。在单线程上运行IO密集型任务会导致浪费大量的CPU运算能力浪费在等待 。
对于计算密集型任务 , 原则是配置尽可能少的线程数 , 通常建议以下计算方式设置线程池大小来获得最优利用率:
N(线程数) = N(CPU核数)+ 1
对于IO密集型任务 , 考虑的因素会多一些 , 原则是因为较多的时间处于IO阻塞 , 不能处理新的任务 , 所有线程数尽可能大一些 , 通常建议是:
N(线程数) = 2 x N(CPU核数) + 1
或者更精确的:
N(线程数) = N(CPU核数) x U x (1 + W/C)
其中U表示CPU使用率 , W/C表示IO等待时间与计算时间的比率 , 这个不需要太精确 , 只需要一个估算值 。例如 , 4核CPU , CPU使用率80% , IO等待时间1秒 , 计算时间0.1秒 , 那么线程数为:4.8 x 11≈53 。一些文章中还提到这种计算方式:
N(线程数) = N(CPU核数) x U / (1 - f)
其中U表示CPU使用率 , f表示阻塞系数 , 即IO等待时间与任务执行总时间的比率:W/(W + C) 。根据上面的例子计算出线程数为:4.8/0.09≈53 。两种计算方式的结果是很相近的 。
 
以上的计算方式和建议尽可以作为理论参考值 , 实际业务中可能并不完全按照这个计算值来设置 。可以根据对线程池各项参数的监控 , 来确定一个合理的值 。ThreadPoolExecutor提供的一些可用于获取监控的参数方法如下:
  • getTaskCount():线程池需要执行的任务数量 , 包括已经执行完的、未执行的和正在执行的 。
  • getCompletedTaskCount():线程池在运行过程中已完成的任务数量  , completedTaskCount <= taskCount 。
  • getLargestPoolSize():线程池曾经创建过的最大线程数量  , 通过这个数据可以知道线程池是否满过 。如等于线程池的最大大小  , 则表示线程池曾经满了 。
  • getPoolSize(): 线程池的线程数量 。如果线程池不销毁的话 , 池里的线程不会自动销毁 , 所以线程池的线程数量只增不减。
  • getActiveCount():获取活动的线程数 。
此外 , 可以通过继承ThreadPoolExecutor并重写它的 beforeExecute() , afterExecute() 和 terminated()方法 , 我们可以在任务执行前 , 执行后和线程池关闭前做一些统计、日志输出等等操作 , 以帮助我们更好地监控到线程池的运行状态 。

【Java线程池】


推荐阅读