为了加快程序处理速度,我们会将问题分解成若干个并发执行的任务。并
且创建线程池,将任务委派给线程池中的线程,以便使它们可以并发地执
行。在高并发的情况下采用线程池,可以有效降低线程创建释放的时间花
销及资源开销,如不使用线程池,有可能造成系统创建大量线程而导致消
耗完系统内存以及“过度切换”(在JVM中采用的处理机制为时间片轮转,减
少了线程间的相互切换) 。
但是有一个很大的问题摆在我们面前,即我们希望尽可能多地创建任务,
但由于资源所限我们又不能创建过多的线程。那么在高并发的情况下,我
们怎么选择最优的线程数量呢?选择原则又是什么呢?
一、理论分析
关于如何计算并发线程数,有两种说法。
第一种,《Java Concurrency in Practice》即《java并发编程实践》8.2节
170页
对于计算密集型的任务,一个有Ncpu个处理器的系统通常通过使用一个Nc
pu +
1个线程的线程池来获得最优的利用率(计算密集型的线程恰好在某时因为
发生一个页错误或者因其他原因而暂停,刚好有一个“额外”的线程,可以
确保在这种情况下CPU周期不会中断工作)。
对于包含了
I/O和其他阻塞操作的任务,不是所有的线程都会在所有的时间被调度,因
此你需要一个更大的池。为了正确地设置线程池的长度,你必须估算出任
务花在等待的时间与用来计算的时间的比率;这个估算值不必十分精确,
而且可以通过一些监控工具获得。你还可以选择另一种方法来调节线程池
的大小,在一个基准负载下,使用
几种不同大小的线程池运行你的应用程序,并观察CPU利用率的水平。
给定下列定义:
Ncpu = CPU的数量
Ucpu = 目标CPU的使用率, 0 <= Ucpu <= 1
W/C = 等待时间与计算时间的比率
为保持处理器达到期望的使用率,最优的池的大小等于:
Nthreads = Ncpu x Ucpu x (1 + W/C)
你可以使用Runtime来获得CPU的数目:
int N_CPUS = Runtime.getRuntime().availableProcessors();
当然,CPU周期并不是唯一你可以使用线程池管理的资源。其他可以约束
资源池大小的资源包括:内存、文件句柄、套接字句柄和数据库连接等。
计算这些类型资源池的大小约束非常简单:首先累加出每一个任务需要的
这些资源的总童,然后除以可用的总量。所得的结果是池大小的上限。
当任务需要使用池化的资源时,比如数据库连接,那么线程池的长度和资
源池的长度会相互影响。如果每一个任务都需要一个数据库连接,那么连
接池的大小就限制了线程池的有效大小;类似地,当线程池中的任务是连
接池的唯一消费者时,那么线程池的大小反而又会限制了连接池的有效大
小。
如上,在《Java Concurrency in
Practice》一书中,给出了估算线程池大小的公式:
Nthreads = Ncpu x Ucpu x (1 + W/C),其中
Ncpu = CPU核心数
Ucpu = CPU使用率,0~1
W/C = 等待时间与计算时间的比率
第二种,《Programming Concurrency on the JVM Mastering》即《Java
虚拟机并发编程》2.1节 12页
为了解决上述难题,我们希望至少可以创建处理器核心数那么多个线程。
这就保证了有尽可能多地处理器核心可以投入到解决问题的工作中去。通
过下面的代码,我们可以很容易地获取到系统可用的处理器核心数:
Runtime.getRuntime().availableProcessors();
所以,应用程序的最小线程数应该等于可用的处理器核数。如果所有的任
务都是计算密集型的,则创建处理器可用核心数那么多个线程就可以了。
在这种情况下,创建更多的线程对程序性能而言反而是不利的。因为当有
多个仟务处于就绪状态时,处理器核心需要在线程间频繁进行上下文切换