线程池合理的长度取决于将要提交的任务类型和所部署系统的特征。很少有人会把线程池的长度硬编码;池的长度应该由某种配置机制来提供,或者利用Runtime.availableProcessors的结果,动态的进行计算。
如果一个线程池过大,那么线程对稀缺的CPU和内存资源的竞争会导致内存的高使用量,还可能耗尽资源。如果过小,由于存在很多可用的处理器资源却未在工作,会对吞吐量造成损失。
为了正确的定制线程池的长度,你需要理解你的计算环境、资源预算和任务的自身特性。部署系统中安装了多少个CPU?多少内存?任务主要执行的是计算、I/O还是一些混合操作?它们是否需要像JDBC Connection这样的稀缺资源?如果你有不同类别的任务,它们拥有差别很大的行为,那么应该考虑使用多个不同的线程池,这样每个线程池可以根据不同任务的工作负载进行调节。
对于计算密集型的任务,一个有Ncpu 个处理器的系统通常通过使用一个Ncpu +1个线程的线程池来获得最优的利用率(计算密集型的线程恰好在某时因为发生一个页错误或者因为其他原因而暂停,刚好有一个“额外”的线程,可以确保在这样的情况下CPU周期不会中断工作)。对于包含了I/O和其他阻塞操作的任务,不是所有的线程都会在所有的时间被调度,因此你需要一个更大的池。为了正确地设置线程池的长度,你必须估算出任务花在等待的时间与用来计算的时间的比率;这个估算值不必十分精确,而且可以通过一些监控工具获得。你还可以选择另一种方法来调节线程池的大小,在一个基准负载下,使用不同大小的线程池运行你的应用程序,并观察CPU利用率的水平。
给定下列定义:
Ncpu = CPU的数量
Ucpu = 目标CPU的使用率,0 ≤ Ucpu≤ 1
W/C = 等待时间与计算时间的比率
为保持处理器达到期望的使用率,最优的池的大小等于:
Nthreads = Ncpu * Ucpu * ( 1 + W/C )
可以使用Runtime来获得CPU的数目:
int N_CPUS = Runtime.getRuntime().availableProcessors();
CPU周期并不是唯一可以使用线程池管理的资源。其他可以约束资源池大小的资源包括:内存、文件句柄、套接字句柄和数据库连接等。计算这些资源池的大小约束非常简单:首先累加出每一个任务需要的这些资源的总量,然后除以可用的总量。所得的结果是池大小的上限。
当任务需要使用池化的资源时,比如数据库连接,那么线程池的长度和资源池的长度会互相影响。如果每一个任务都需要一个数据库连接,那么线程池的大小就限制了线程池的有效大小;类似的,当线程池中的任务是线程池的唯一消费者时,那么线程池的大小反而又会限制了连接池的有效大小。
参见《java并发编程实践》 第八章