线程池和ThreadPoolExecutors
虽然在程序中可以直接使用Thread类型来进行线程操作,但是更多的情况是使用线程池,尤其是在Java EE应用服务器中,一般会使用若干个线程池来处理来自客户端的请求。Java中对于线程池的支持,来自ThreadPoolExecutor。一些应用服务器也确实是使用的ThreadPoolExecutor来实现线程池。
对于线程池的性能调优,最重要的参数就是线程池的大小。
对于任何线程池而言,它们的工作方式几乎都是相同的:
- 任务被投放到一个队列中(队列的数量不定)
- 线程从队列中取得任务并执行
- 线程完成任务后,继续尝试从队列中取得任务,如果队列为空,那么线程进入等待状态
线程池往往拥有最小和最大线程数:
- 最小线程数,即当任务队列为空时,线程池中最少需要保持的线程数量,这样做是考虑到创建线程是一个相对耗费资源的操作,应当尽可能地避免,当有新任务被投入队列时,总会有线程能够立即对它进行处理。
- 最大线程数,当需要处理的任务过多时,线程池能够拥有的最大线程数。这样是为了保证不会有过多的线程被创建出来,因为线程的运行需要依赖于CPU资源和其它各种资源,当线程过多时,反而会降低性能。
在ThreadPoolExecutor和其相关的类型中,最小线程数被称为线程池核心规模(Core Pool Size),在其它Java应用服务器的实现中,这个数量也许被称为最小线程数(MinThreads),但是它们的概念是相同的。
但是在对线程池进行规模变更(Resizing)的时候,ThreadPoolExecutor和其它线程池的实现也许存在的很大的差别。
一个最简单的情况是:当有新任务需要被执行,且当前所有的线程都被占用时,ThreadPoolExecutor和其它实现通常都会新创建一个线程来执行这个新任务(直到达到了最大线程数)。
设置最大线程数
最合适的最大线程数该怎么确定,依赖以下两个方面:
- 任务的特征
- 计算机的硬件情况
为了方便讨论,下面假设JVM有4个可用的CPU。那么任务也很明确,就是要最大程度地“压榨”它们的资源,千方百计的提高CPU的利用率。
那么,最大线程数最少需要被设置成4,因为有4个可用的CPU,意味着最多能够并行地执行4个任务。当然,垃圾回收(Garbage Collection)在这个过程中也会造成一些影响,但是它们往往不需要使用整个CPU。一个例外是,当使用了CMS或者G1垃圾回收算法时,需要有足够的CPU资源进行垃圾回收。
那么是否有必要将线程数量设置的更大呢?这就取决于任务的特征了。
假设当任务是计算密集型的,意味着任务不需要执行IO操作,例如读取数据库,读取文件等,因此它们不涉及到同步的问题,任务之间完全是独立的。比如使用一个批处理程序读取Mock数据源的数据,测试在不线程池拥有不同线程数量时的性能,得到下表:
线程数 | 执行时间(秒) | 基线百分比 |
---|---|---|
1 | 255.6 | 100% |
2 | 134.8 | 52.7% |
4 | 77.0 | 30.1% |
8 | 81.7 | 31.9% |
16 | 85.6 | 33.5% |
从上面中得到一些结论:
- 当线程数为4时,达到最优性能,再增加线程数量时并没有更好的性能,因为此时CPU的利用率已经达到了最高,在增加线程只会增加线程之间争夺CPU资源的行为,因此反而降低了性能。
- 即使在CPU利用率达到最高时,基线百分比也不是理想中的25%,这是因为虽然在程序运行过程中,CPU资源并不是只被应用程序线程独享的,一些后台线程有时也会需要CPU资源,比如GC线程和系统的一些线程等。
当计算是通过Servlet触发的时候,性能数据是下面这个样子的(Load Generator会同时发送20个请求):
线程数 | 每秒操作数(OPS) | 基线百分比 |
---|---|---|
4 | 77.43 | 100% |
8 | 75.93 | 98.8% |
16 | 71.65 | 92.5% |
32 | 69.34 | 89.5% |
64 | 60.44 | 78.1% |
从上表中可以得到的结论:
- 在线程数量为4时,性能最优。因为此任务的类型是计算密集型的,只有4个CPU,因此线程数量为4时,达到最优情况。
- 随着线程数量逐渐增加,性能下降,因为线程之间会互相争夺CPU资源,造成频繁切换线程执行上下文环境,而这些切换只会浪费CPU资源。
- 性能下降的速度并不明显,这也是因为任务类型是计算密集型的缘故,如果性能瓶颈不是CPU提供的计算资源,而是外部的资源,如数据库,文件操作等,那么增加线程数量带来的性能下降也许会更加明显。
下面,从Client的角度考虑一下问题,并发Client的数量对于Server的响应时间会有什么影响呢?还是同样地环境,当并发Client数量逐渐增加时,响应时间会如下发生变化:
并发Client线程数 | 平均响应时间(秒) | 基线百分比 |
---|---|---|
1 | 0.05 | 100% |
2 | 0.05 | 100% |
4 | 0.05 | 100% |
6 | 0.076 | 152% |
8 | 0.104 | 208% |