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

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

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

当池中只有一个线程时,计算所花的时间基本一样。这可以理解:不管池如何实现,计算量是一样的;而且因为那些计算绝对不会并行进行,所以可以预计它们所需的时间是一样的(尽管创建 200 万个任务会有少量开销)。但是当池中包含 4 个线程时,ForkJoinPool 中任务的粒度会带来一个决定性的优势:几乎在测试的整个过程中,都能保持 CPU 的忙碌状态。

这种情况就叫作“不均衡”,因为某些任务所花的时间比其他任务长(因此前面例子中的任务可以说是“均衡的”)。一般而言,如果任务是均衡的,使用分段的 ThreadPoolExecutor 性能更好;而如果任务是不均衡的,则使用 ForkJoinPool 性能更好。

还有一个更微妙的性能方面的建议:请仔细考虑 fork/join 范型应该在哪个点结束递归。在这个例子中,我信手选择了当数组大小小于 10 时结束。如果在数组大小为 250 万时停止递归,那么 fork/join 测试(在搭载 4 个 CPU 的机器上,处理 1000 万个元素的平衡代码)会只创建 4 个任务,其性能基本和 ThreadPoolExecutor 一样。

另一方面,对于这个例子,在非平衡的测试中,继续递归会有更好的性能,即使创建更多任务。表6 给出了一些有代表性的数据点。

表6:处理包含10 000个元素的数组的时间

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

自动并行化

Java 8 向 Java 中引入了自动并行化特定种类代码的能力。这种并行化就依赖于 ForkJoinPool 类的使用。Java 8 为这个类加入了一个新特性:

一个公共的池,可供任何没有显式指定给某个特定池的 ForkJoinTask 使用。

这个公共池是 ForkJoinPool 类的一个 static 元素,其大小默认设置为目标机器上的处理器数。

这种并行化在 Arrays 类的很多新方法中都会发生,包括使用并行快速排序处理数组的方法,操作数组的每个元素的方法,等等。在 Java 8 的 Stream 特性中也有应用,支持在集合中的每个元素上(或顺序或并行地)执行操作。这里不讨论Stream 的一些基本的性能特性,而是看一下 Stream 是如何自动地并行处理的。

给定一个包含一系列整型数的集合,下列代码会计算与给定整型数匹配的股票代号的价格历史:

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

这段代码会并行计算模拟价格历史:forEach 方法将为数组列表中的每个元素创建一个任务,每个任务都会由公共的 ForkJoinTask 池处理。它在功能上与开始所做的测试是等价的,那个测试是用一个线程池来并行计算价格历史(不过与显式使用线程池相比,这段代码写起来更容易)。

设置 ForkJoinTask 池的大小和设置其他任何线程池同样重要。默认情况下,公共池的线程数等于机器上的 CPU 数。如果在同一机器上运行着多个 JVM,则应限制这个线程数,以防这些 JVM 彼此争用 CPU。类似地,如果 Servlet 代码会执行某个并行任务,而我们想确保 CPU 可供其他任务使用,可以考虑减小公共池的线程数。另外,如果公共池中的任务会阻塞等待 I/O 或其他数据,也可以考虑增大线程数。

这个值可以通过设置系统属性 -Djava.util.concurrent.ForkJoinPool.common.parallelism=N 来指定。

前面的表1 中,曾经对比过线程数对并行计算股票历史价格的影响。表7 使用共同的ForkJoinPool(将 parallelism 系统属性设置为给定的值)将那个数据与 forEach 构造作了比较。

表7:计算10 000支模拟股票价格历史所需的时间

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

默认情况下,公共池有 4 个线程(在这个配置了 4 个 CPU 的机器上),所以表中的第 3 行为一般情况。在线程数为 1 和 2 时,这类结果会让性能工程师很不开心:它们看上去很不协调,而当某一项测试出现这样的情况时,最常见的原因是测试错误。这里的原因是 forEach 方法有些奇怪的行为:它使用了一个线程执行语句,还使用了公共池中的线程处理来自 Stream 的数据。即使在第 1 个测试中,公共池也是配置为使用一个线程,总的还是会使用两个线程来计算结果。(因此,使用了 2 个线程的 ThreadPoolExecutor 和使用了 1 个线程的 ForkJoinPool 的耗时基本相同。)

在使用并行 Stream 构造或其他自动并行化特性时,如果需要调整公共池的大小,可以考虑将所需的值减 1。

快速小结

ForkJoinPool 类应该用于递归、分治算法。

应该花些心思来确定,算法中的递归任务何时结束最为合适。创建太多任务会降低性能,但如果任务太少,而任务所需的执行时间又长短不一,也会降低性能。

Java 8 中使用了自动并行化的特性会用到一个公共的 ForkJoinPool 实例。我们可能需要根据实际情况调整这个实例的默认大小。

理解线程如何运作,可以获得很大的性能优势。不过就线程的性能而言,其实没有太多可以调优的:可以修改的 JVM 标志相当少,而且那些标志的效果也很有限。

相反,较好的线程性能是这么来的:遵循管理线程数、限制同步带来的影响的一系列最佳实践原则。借助适当的剖析工具和锁分析工具,可以检查并修改应用,以避免线程和锁的问题给性能带来负面影响。

调节线程栈大小

(编辑:源码网)

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

热点阅读