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

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

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

因此,对于容纳等待执行任务的队列,线程池通常会限制其大小。根据用于容纳等待执行任务的数据结构的不同,ThreadPoolExecutor 会有不同的处理方式(下一节会更详细地介绍);应用服务器通常有一些调优参数,可以调整这个值。

就像线程池的最大线程数,这个值应该如何调优,并没有一个通用的规则。举例而言,假设某个应用服务器的任务队列中有 30 000 个任务,有 4 个 CPU 可用,如果执行一个任务只需要 50 毫秒,同时假设这段时间不会到达新任务,则清空任务队列需要 6 分钟。这可能是可以接受的,但如果每个任务需要 1 秒钟,则清空任务队列需要 2 小时。因此,若要确定使用哪个值能带来我们需要的性能,测量我们的真实应用是唯一的途径。

不管是哪种情况,如果达到了队列数限制,再添加任务就会失败。ThreadPoolExecutor 有一个rejectedExecution 方法,用于处理这种情况(默认会抛出 RejectedExecutionException)。应用服务器会向用户返回某个错误:或者是 HTTP 状态码 500(内部错误),或者是 Web 服务器捕获错误,并向用户给出合理的解释消息——其中后者是最理想的。

设置 ThreadPoolExecutor 的大小

线程池的一般行为是这样的:

  • 创建时准备好最小数目的线程,如果来了一个任务,而此时所有的线程都在忙碌,则启动一个新线程(一直到达到最大线程数),任务就可以立即执行了。
  • 否则,任务被加入等待队列,如果任务队列中已经无法加入新任务,则拒绝之。

不过,ThreadPoolExecutor的表现可能和这种标准行为有点不同。

根据所选任务队列的类型,ThreadPoolExecutor 会决定何时启动一个新线程。有以下 3 种可能。

1. SynchronousQueue

如果 ThreadPoolExecutor 搭配的是 SynchronousQueue,则线程池的行为会和我们预计的一样,它会考虑线程数:如果所有的线程都在忙碌,而且池中的线程数尚未达到最大,则新任务会启动一个新线程。然而,这个队列没办法保存等待的任务:如果来了一个任务,创建的线程数已经达到最大值,而且所有线程都在忙碌,则新的任务总是会被拒绝。所以如果只是管理少量的任务,这是个不错的选择;但是对于其他情况,就不合适了。该类文档建议将最大线程数指定为一个非常大的值,如果任务完全是 CPU 密集型的,这可能行得通,但是我们会看到,其他情况下可能会适得其反。另一方面,如果需要一个容易调整线程数的线程池,这种选择会更好。

2. 无界队列

如果 ThreadPoolExecutor 搭配的是无界队列(比如 LinkedBlockedingQueue),则不会拒绝任何任务(因为队列大小没有限制)。这种情况下,ThreadPoolExecutor 最多仅会按最小线程数创建线程,也就是说,最大线程池大小被忽略了。如果最大线程数和最小线程数相同,则这种选择和配置了固定线程数的传统线程池运行机制最为接近。

3. 有界队列

在决定何时启动一个新线程时,使用了有界队列(如 ArrayBlockingQueue)的ThreadPoolExecutor 会采用一个非常复杂的算法。比如,假设池的核心大小为 4,最大为 8,所用的 ArrayBlockingQueue 最大为 10。随着任务到达并被放到队列中,线程池中最多会运行 4 个线程(也就是核心大小)。即使队列完全填满,也就是说有 10 个处于等待状态的任务,ThreadPoolExecutor 也是只利用 4 个线程。

如果队列已满,而又有新任务加进来,此时才会启动一个新线程。这里不会因为队列已满而拒绝该任务,相反,会启动一个新线程。新线程会运行队列中的第一个任务,为新来的任务腾出空间。

在这个例子中,池中会有 8 个线程(最大线程数)的唯一一种情形是,有 7 个任务正在处理,队列中有 10 个任务,这时又来了一个新任务。

这个算法背后的理念是,该池大部分时间仅使用核心线程(4 个),即使有适量的任务在队列中等待运行。这时线程池就可以用作节流阀(这是很有好处的)。如果积压的请求变得非常多,该池就会尝试运行更多线程来清理;这时第二个节流阀——最大线程数——就起作用了。

如果系统没有外部瓶颈,CPU 周期也足够,那一切就都解决了:加入新的线程可以更快地处理任务队列,并很可能使其回到预期大小。该算法所适合的用例当然也很容易构造。

另一方面,该算法并不知道队列为何会突然增大。如果是因为外部的任务积压,那么加入更多线程并非明智之举。如果该线程所运行的机器已经是 CPU 密集型的,加入更多线程也是错误的。只有当任务积压是由额外的负载进入系统(比如有更多客户端发起 HTTP 请求)引发时,增加线程才是有意义的。(如果是这种情况,为什么要等到队列已经接近某个边界时才增加呢?如果有额外的资源供更多线程使用,则尽早增加线程将改善系统的整体性能。)

对于上面提到的每一种选择,都能找到很多支持或反对的论据,但是在尝试获得最好的性能时,可以应用 KISS 原则“Keep it simple, stupid”。可以将 ThreadPoolExecutor 的核心线程数和最大线程数设为相同,在保存等待任务方面,如果适合使用无界任务列表,则选择 LinkedBlockingQueue;如果适合使用有界任务列表,则选择 ArrayBlockingQueue。

快速小结

有时对象池也是不错的选择,线程池就是情形之一:线程初始化的成本很高,线程池使得系统上的线程数容易控制。

线程池必须仔细调优。盲目向池中添加新线程,在某些情况下对性能会有不利影响。

在使用 ThreadPoolExecutor 时,选择更简单的选项通常会带来最好的、最能预见的性能。

ForkJoinPool

Java 7 引入了一个新的线程池:ForkJoinPool 类。这个类看上去和其他任何线程池都很像;和 ThreadPoolExecutor 类一样,它也实现了 Executor 和 ExecutorService 接口。在支持这些接口方面,ForkJoinPool 在内部会使用一个无界任务列表,供构造器中所指定数目(如果所选的是无参构造器,则为该机器上的 CPU 数)的线程来运行。

ForkJoinPool 类是为配合分治算法的使用而设计的:任务可以递归地分解为子集。这些子集可以并行处理,然后每个子集的结果被归并到一个结果中。一个经典的例子就是快速排序算法。

(编辑:源码网)

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

热点阅读