加入收藏 | 设为首页 | 会员中心 | 我要投稿 源码网 (https://www.900php.com/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 站长资讯 > 外闻 > 正文

为何服务器QPS上不去?Java线程调优权威指南

发布时间:2019-07-16 16:20:21 所属栏目:外闻 来源:今日头条
导读:副标题#e# 从刚问世起,Java 的部分魅力就来自其多线程。即便在多核和多 CPU 系统司空见惯之前,能够轻松编写多线程程序也是 Java 的一个标志性特征。 Java 性能方面的吸引力显而易见:如果有两个 CPU 可用,那么一个应用能够完成的工作量可能是原来的 2 倍

这个例子看上去可能有点有意为之。如果服务器已经是 CPU 密集型的,谁还会加入更多线程呢?之所以使用这个例子,只是因为它容易理解,而且仅使用了 Java 程序。这意味着读者自己就可以运行,并理解它是如何工作的,而不必设置数据库连接、模式(Schema)等选项。

需要指出的是,对于还要向 CPU 密集型或 I/O 密集型的机器发送数据库请求的应用服务器而言,同样的原则也成立。你可能只关注应用服务器 CPU,看到小于 100% 就感觉不错;看到有多余的请求要处理,就假定增加应用服务器的线程数是个不错的主意。结果会让人大吃一惊,因为在那种情况下增加线程数,实际上会降低整体吞吐量(影响可能非常明显),就像前面那个只有 Java 程序的例子一样。

了解系统真正瓶颈之所在非常重要的另一个原因是:

  • 如果还向瓶颈处增加负载,性能会显著下降。
  • 相反,如果减少了当前瓶颈处的负载,性能可能会上升。

这也是设计自我调优的线程池非常困难的原因所在。线程池通常对挂起了多少工作有所了解,甚至有多少 CPU 可用也可以知道,但是它们通常看不到所在的整个环境的其他方面。因此,当有工作挂起时,增加线程(这是很多自我调优的线程池的一个核心特性,也是 ThreadPoolExecutor 的某些配置)往往是完全错误的。

遗憾的是,设置最大线程数更像是艺术而非科学,原因也在于此。在现实中,测试条件下自我调优的线程池会实现可能性能的 80%~90%;而且就算高估了所需线程数,也可能只有很小的损失。但是当设置线程数大小这方面出了问题时,系统可能会在很大程度上出现问题。就此而言,充足的测试仍然非常关键。

设置最小线程数

一旦确定了线程池的最大线程数,就该确定所需的最小线程数了。大部分情况下,开发者会直截了当地将它们设置为同一个值。

将最小线程数设置为其他某个值(比如 1),出发点是防止系统创建太多线程,以节省系统资源。因为每个线程都需要一定量的内存,特别是线程的栈。根据一般原则之一,所设置的系统大小应该能够处理预期的最大吞吐量,而要达到最大吞吐量,系统将需要创建所有那些线程。如果系统做不到这一点,那选择一个最小线程数也没什么帮助:如果系统达到了这样的条件——需要按所设置的最大线程数启动所有线程,而又无法满足,系统将陷入困境。创建最终可能会需要的所有线程,并确保系统可以处理预期的最大负载,这样更好。

另一方面,指定一个最小线程数的负面影响相当小。如果进程一启动就有很多任务要执行,会有负面影响:这时线程池需要创建新线程才能处理任务。创建线程对性能不利,这也是为什么起初需要线程池的原因,不过这种一次性的成本在性能测试中很可能察觉不到。

在批处理应用中,线程是在创建线程池时分配(如果将最大线程数和最小线程数设置为同一个值,就会出现这种情况),还是按需分配,并不重要:执行应用所需的时间是一样的。在其他应用中,新线程可能会在预热阶段分配(分配线程的总时间还是一样的),对性能的影响可以忽略不计。即使线程创建发生在可以测量的周期内,只要此类操作有限,也很有可能测不出来。

另一个可以调优的地方是线程的空闲时间。比如,某个线程池的最小线程数为 1,最大线程数为 4。现在假设一般会有一个线程在执行,处理一个任务;然后应用进入这样一个循环:每 15 秒,负载平均有 2 个任务要执行。第一次进入这个循环时,线程池会创建第 2 个线程,此时,让这个新创建的线程在池中至少留存一段时间是有意义的。我们希望避免这种情况:第 2 个线程创建出来后,5 秒钟内结束其任务,空闲 5 秒,然后退出了。而 5 秒之后又需要为下一个任务创建一个线程。一般而言,对于线程数为最小值的线程池,一个新线程一旦创建出来,至少应该留存几分钟,以处理任何负载飙升。如果任务到达率有个比较好的模型,可以基于这个模型设置空闲时间。另外,空闲时间应该以分钟计,而且至少在 10 分钟到 30 分钟之间。

留存一些空闲线程,对应用性能的影响通常微乎其微。一般而言,线程对象本身不会占用大量的堆空间。除非线程保持了大量的线程局部存储,或者线程的 Runnable 对象引用了大量内存。不管是哪种情况,释放这样的线程都会显著减少堆中的活数据(这反过来又会影响 GC 的效率)。

不过对线程池而言,这些情况并不多见。当池中的某个对象空闲时,它就不应该再引用任何 Runnable 对象(如果引用了,就说明哪个地方有 bug 了)。根据线程池的实现情况,线程局部变量可能会继续保留;尽管在某些情况下,线程局部变量可以有效促成对象重用,但是那些线程局部对象所占用的总的内存量,应该加以限制。

对于可能会增长到非常大(当然也是运行在规模很大的机器上)的线程池,这个规则有个重要的特例。举例而言,假设某个线程池的任务队列预计平均有 20 个任务,那么 20 就是很好的最小值。再假设这个池运行在一个规模很大的机器上,它被设计为可以处理 2000 个任务的峰值负载。如果在池中留存 2000 个空闲线程,则当只有 20 个任务时,对性能会有所影响:如果只有核心的 20 个线程忙碌,与有 1980 个空闲线程相比,前者的吞吐量可能是后者的 50%。线程池一般不会遇到这样的问题,但如果遇到了,那就应该确认一下池的合适的最小值了。

线程池任务大小

(编辑:源码网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

热点阅读