风也温柔

计算机科学知识库

解决方案:Java web使用线程池_Java web应用中调优线程池的重要性

  无论您是否注意,Java Web 应用程序都或多或少地使用线程池来处理请求。线程池的实现细节可能会被忽略,但迟早需要了解线程池的使用和调优。本文主要介绍Java线程池的使用以及如何正确配置线程池。

  单线程

  让我们先从基础开始。无论使用何种应用服务器或框架(例如,Jetty 等),它们都具有相似的基本实现。web服务的基础是(),负责监听一个端口,等待一个TCP连接,接受一个TCP连接。一旦 TCP 连接被接受,就可以从新创建的 TCP 连接中读取和发送数据。

  为了能够理解上述流程,我们不直接使用任何应用服务器,而是从头构建一个简单的 Web 服务。该服务是大多数应用服务器的缩影。一个简单的单线程 Web 服务可能如下所示:

  6254fcc70ec18bc05cccff2e4b4089bb.png

  上面的代码创建了一个服务器(),监听8080端口,然后循环这个,看看有没有新的连接。一旦接受新连接,此套接字就会传递给该方法。该方法将数据流解析为HTTP请求,响应,写入响应数据。在这个简单的示例中,该方法只是读取数据流并返回一个简单的响应。在常见的实现中,这种方法也复杂得多,例如从数据库中读取数据。

  e2dae235f1a44728da4e8a1be1fbdcee.png

  由于只有一个线程来处理请求,因此每个请求必须等待前一个请求完成后才能得到响应。假设请求响应时间为 100 毫秒,那么服务器每秒的响应 (tps) 仅为 10。

  多线程

  尽管该方法可能会阻塞 IO,但 CPU 仍然可以处理更多请求。但是在单线程的情况下,这是做不到的。因此,可以通过创建多个线程来提高服务器的并行处理能力。

  f23f2ab9d27cbbc869e03e480cfd2416.png

  这里,() 方法仍然是在主线程中调用的,但是一旦建立了 TCP 连接,就会创建一个新的线程来处理新的请求,也就是在新的线程中执行之前的方法。

  通过创建新线程,主线程可以继续接受新的 TCP 连接,这些请求可以并行处理。这种方法称为“每个请求一个线程( per )”。当然,还有其他方法可以提高处理性能,例如 NGINX 和 Node.js 使用的异步事件驱动模型,但它们不使用线程池,因此超出了本文的范围。

  在每个请求线程的实现中,线程的创建(以及随后的销毁)非常昂贵解决方案:Java web使用线程池_Java web应用中调优线程池的重要性,因为 JVM 和操作系统都需要分配资源。另外,上述实现还有一个问题,就是创建的线程数量不可控,可能会导致系统资源快速耗尽。

  资源枯竭

  每个线程都需要一定数量的堆栈内存空间。在最近的 64 位 JVM 中,默认堆栈大小为 . 如果服务器接收到大量的请求,或者方法执行速度很慢,服务器可能会因为创建大量线程而崩溃。例如,有 1000 个并行请求,创建的 1000 个线程需要使用 1GB 的 JVM 内存作为线程堆栈空间。此外,在每个线程的代码执行期间创建的对象也可能在堆上创建对象。如果这种情况恶化,就会超出JVM堆内存,产生大量垃圾回收操作,最终导致内存溢出()。

  这些线程不仅消耗内存,它们还使用其他有限的资源,例如文件句柄、数据库连接等。不受控制的线程创建也可能导致其他类型的错误和崩溃。因此,避免资源耗尽的一个重要方法是避免不可控的数据结构。

  顺便说一句,由于线程堆栈大小引起的内存问题,可以通过-Xss开关调整堆栈大小。减少线程堆栈大小后,可以减少每个线程的开销,但可能会导致堆栈溢出()。对于一般的应用,默认的太丰富了,把它缩小到 256KB 或者 512KB 可能比较合适。Java 允许的最小值为 160KB。

  线程池

  为了避免不断创建新线程,可以使用简单的线程池来限制线程池。线程池将管理所有线程。如果线程数没有达到上限,线程池会创建到上限的线程,并尽可能重用空闲线程。

  aef879dfa0a82b1a3cfa713f54c32acb.png

  在这个例子中,线程不是直接创建的,而是被使用的。它将需要执行的任务(需要实现的接口)提交给线程池,并使用线程池中的线程来执行代码。在示例中,使用线程数为 4 的固定大小的线程池来处理所有请求。这限制了处理请求的线程数,也限制了资源的使用。

  该类除了通过方法创建固定大小的线程池外,还提供了方法。重用线程池可能仍然会导致线程数不可控,但它会尽量使用之前创建的空闲线程。通常这种类型的线程池适用于不会被外部资源阻塞的短任务。

  工作队列

  使用固定大小的线程池后,如果所有线程都忙,那么新的请求会发生什么?队列用于保存待处理的请求,固定大小的线程池默认使用无限链表。请注意,这又会导致资源耗尽问题,但只要线程的处理速度快于队列的增长速度,就不会发生这种情况。然后在前面的示例中,每个排队的请求都将持有一个套接字,在某些操作系统中,该套接字会消耗一个文件句柄。由于操作系统限制了一个进程可以打开的文件句柄数,因此最好限制工作队列的大小。

  d5e792dc784f34a90cdb3ef0973cf6f3.png

  这里我们不使用 . 方法直接创建线程池,但自己构建对象并将工作队列长度限制为 16 个元素。

  如果所有线程都忙,则新任务将被填充到队列中。由于队列的大小限制为 16 个元素,如果超过这个限制,则需要在构造对象时通过最后一个参数进行处理。示例中使用了 (),即当队列达到上限时,新的任务将被丢弃。除了第一次,还有abort ()和 ()。前者会抛出异常,而后者会在调用者的线程中执行任务。

  对于 Web 应用程序,最佳的默认策略应该是丢弃或中止该策略并向客户端返回错误(例如 HTTP 503 错误)。当然也可以增加工作队列的长度来避免放弃客户端请求,但是用户请求一般都不愿意等待很长时间,这样会消耗更多的服务器资源。工作队列的目的不是无限制地响应客户端请求,而是平滑突发的请求。通常,工作队列应该是空的。

  线程数调整

  前面的例子展示了如何创建和使用线程池,但是使用线程池的核心问题是应该使用多少个线程。首先,我们要确保在达到线程限制时不会耗尽资源。这里的资源包括内存(堆和栈)、打开文件句柄数、TCP连接数、远程数据库连接数等有限资源。特别是,如果线程任务是计算密集型的java 线程数量上限,那么 CPU 内核的数量也是资源限制之一。一般来说,线程数不应超过 CPU 核心数。

  由于线程数量的选择取决于应用程序的类型,因此可能需要进行大量的性能测试才能获得最佳结果。当然,也可以通过增加资源数量来提高应用程序的性能。比如修改JVM堆内存的大小,或者修改操作系统的文件句柄上限。那么,这些调整最终会达到理论上限。

  小法则

  利特尔定律描述了稳定系统中三个变量之间的关系。

  其中 L 表示平均请求数,λ 表示请求频率,W 表示响应请求的平均时间。例如,如果每秒的请求数为 10,并且每个请求需要 1 秒的时间来处理,那么任何时候都会处理 10 个请求。回到我们的主题,我们需要使用 10 个线程进行处理。如果单个请求的处理时间增加一倍,那么处理的线程数也增加一倍,达到 20 个。

  在了解了处理时间对请求处理效率的影响之后,我们会发现通常理论上限可能并不是线程池大小的最优值。线程池的上限也需要参考任务处理时间。

  假设JVM可以并行处理1000个任务,如果每个请求处理时间不超过30秒,那么在最坏的情况下,每秒最多只能处理33.3个请求。但是,如果每个请求只需要 500 毫秒,那么应用程序每秒可以处理 2000 个请求。

  拆分线程池

  在微服务或面向服务的架构 (SOA) 中,通常需要访问多个后端服务。如果其中一个服务降级,可能会导致线程池线程耗尽,影响对其他服务的请求。

  处理后端服务故障的一个有效方法是隔离各个服务使用的线程池。在这种模式下,仍然有一个分派线程池,将任务分派到不同的后端请求线程池。由于后端缓慢,线程池可能负载不足,从而将负担转移到请求缓慢后端的线程池上。

  此外,多线程池模式也需要避免死锁问题。当每个线程都阻塞等待未处理请求的结果时,就会发生死锁。因此,在多线程池模式下,需要了解每个线程池执行的任务以及它们之间的依赖关系java 线程数量上限,从而尽可能避免死锁问题。

  总结

  即使线程池不直接在应用程序中使用,它们也很可能被应用程序服务器或应用程序中的框架间接使用。, JBoss , 和其他框架,都提供了调整线程池(执行使用的线程池)的选项。

  希望这篇文章可以提高对线程池的理解。通过了解应用的需求,结合最大线程数和平均响应时间,可以推导出合适的线程池配置。

  文章来源:https://blog.csdn.net/weixin_30234101/article/details/114564281