Java 线程和内核数

suj*_*ith 1 concurrency multithreading cpu-cores

是否建议java应用程序中的线程数应小于cpu内核数?

如果是这样,为什么会出现这种情况以及使用大于 cpu 内核数的线程的含义是什么?

GPI*_*GPI 6

对于了解应用程序应该有多少线程与底层计算机拥有的内核数量相关的问题,您可能不会得到任何明确的答案。

也有人可能会争辩说,在 PaaS 软件设计和/或弹性集群时,任何给定进程的固定核数的概念可能被高估了。

不过,你的问题的第一部分:

是否建议java应用程序中的线程数应小于cpu内核数?

这有一个明确的答案,即“否”(再一次:作为一般规则)。为什么呢,不久,它是所有创建的线程通常不运行(也许更重要的是运行的原因)一次,这意味着这里有个机会来优化。

作为对本次讨论的支持,我将反对两种创建应用程序的方式,您可以将其称为“经典”与“反应式”,尽管这不是普遍可接受的划分。然而,让我们用它作为支持。

经典应用设计

我将其标记为主要依赖“阻塞”调用和/或“每个请求的线程”模式的经典应用程序。考虑完成 I/O 的传统方式(套接字通信,如 HTTP 或数据库连接,基于硬盘驱动器的文件读取,...):您的应用程序线程调用某种readwrite方法,通常会触发操作系统级调用,从而阻止您app 线程,在操作系统级别填充一些设备缓冲区(例如,从磁盘读取)。一旦缓冲区接收到足够的数据,操作系统就会向您的 Java 应用程序和线程发出信号以恢复活动,并且该read方法将返回缓冲区中的数据。

在操作系统工作的整个过程中(通常只是一秒的一小部分,但与典型的 GHz CPU 速度相比仍然需要大量时间),您的 Java 线程处于 state BLOCKED_WAITING,等待操作系统发出可以恢复的信号。这事儿常常发生。代码分析器工具,如 JProfiler 或 YourKit,可以帮助您测量这个时间。如果您这样做,您会注意到在许多执行 I/O 的应用程序中,这是所谓的“挂墙时间”或“时钟时间”的重要组成部分,用于……等待。

所以我们有一个线程在等待,这意味着它没有使用任何 CPU 时间。它可以被调度,操作系统可以自由地将 CPU 时间分配给其他任何人。

假设这是一个单核 CPU,那么现在是让另一个线程为 CPU 供电的好时机。这意味着拥有两个或更多线程可能是一个很好的设计,即使在单核 CPU 上也能最大限度地提高 CPU 使用率,并充分利用您的硬件。

如果您遵循“每个 CPU 内核一个线程”的规则,大多数“经典”Web 应用程序通常会受到这种类型的 CPU 使用不足的影响,因为套接字通信(或更常见的是:等待对 SQL 查询的响应所花费的时间)将招致如此多的阻塞。
如果您增加应用程序的线程数,那么即使一两个长时间运行的请求仍在等待,其他更快的请求也会有可运行的线程来运行它们,并且您将获得更好的 CPU 使用率和更好的性能(并发数要求)。那就是......直到其他东西达到饱和(你的数据库上有太多繁重的请求,太多的硬盘同时读/写......)

反应式应用程序设计

认识到应用程序的这种典型行为,并使用不同的操作系统功能集,一些应用程序框架现在使用非阻塞模式(甚至对于 I/O)来缓解上述问题。Java 生态系统中的示例是基于 NIO 的网络堆栈(如 Netty)或角色模式实现(如 Akka)。

在典型的“反应式”应用程序中,通常会放弃我们在经典应用程序中使用的“每个请求线程”模式(意味着一个线程负责处理给定用户请求从开始到结束的所有内容,并在需要时等待外部资源变为可用),以有利于极大地更加模块化,以及-blocking方法。

线程被赋予了更多技术粒度的工作要做,每个线程将把工作交给另一个线程,并在它们所依赖的工作完成时进行回调。这种工作单元的“处理”意味着每个线程可以快速获取它能够处理的新工作单元。这意味着两件事之一:您可以在应用程序中使用更少的线程获得更高的 CPU 使用率(因为每个线程都可以更有效地获取工作,而不仅仅是坐在“等待”中);或者您可以实例化更多线程,因为它们大部分时间都在等待(不会使 CPU 饱和),并且动态切换仍将允许良好的 CPU 使用率。

结论

无论如何,您不能仅根据可用内核数来设计线程数。您的实现和工作的性质决定了要创建的最佳线程的数量。

在经典的应用程序设计哲学中,这两个数字比响应式数字更密切相关,但我们仍然有不同的配置文件:

  1. 一个非常简单的服务器应用程序可以容纳比 CPU 内核多得多的线程,因为它可以提供更好的吞吐量(限制是输出网络带宽)。
  2. 一个 SQL 繁重的应用程序,应该被调用到您的应用程序服务器将使 SQL 后端饱和的程度。由于您的应用服务器将主要等待您的 SQL 服务器,因此这是限制
  3. 由一些 SQL 繁重工作和一些轻量级工作组成的混合应用程序将需要精确调整,因为您不希望卡住的线程(那些阻塞等待 DB 的线程)饿死将更快地提供服务的轻请求
  4. 计算密集型程序(例如,加密服务)可能会受益于接近 CPU 内核数的线程数(如果您的算法以经典方式实现),因为创建超过您能够运行的线程数是毫无意义的。在基于actor 的实现中,创建更多线程实际上可能是一个胜利。