1. 概述

线程是Java中并发的基本单位。在大多数情况下,当创建多个线程并行执行任务时,应用程序的吞吐量会增加。

然而,总有一个饱和点。毕竟,应用程序的吞吐量取决于 CPU 和内存资源。 达到一定限制后,增加线程数可能会导致内存过高、线程上下文切换等。

因此,解决 Java 应用程序中的高内存问题的一个好的起点是监视线程数。在本教程中,我们将介绍一些检查 Java 进程创建的线程数的方法。

2. 图形化Java监控工具

查看 Java 中线程数的最简单方法是使用Java VisualVM等图形工具。 除了应用程序线程之外,Java VisualVM 还列出了 GC 或应用程序使用的任何其他线程,例如 JMX 线程

此外,它还显示线程状态及其持续时间等统计信息:

Java可视化虚拟机

监控线程数是Java VisualVM中最基本的功能。一般来说,图形工具更先进,可以实时监控应用程序。例如,Java VisualVM 允许我们对 CPU 堆栈跟踪进行采样,从而找到可能导致 CPU 瓶颈的类或方法。

Java VisualVM 随 Windows 计算机上的 JDK 安装一起分发。 对于部署在Linux上的应用程序,我们需要远程连接到应用程序。这需要JMX VM 参数。

因此,如果应用程序已经在没有这些参数的情况下运行,则此类工具将无法工作。在后面的部分中,我们将了解如何使用命令行工具获取线程数。

3.Java API

在某些用例中,我们可能希望找到应用程序本身内的线程数。例如,显示在监控仪表板上或在日志中公开。

在这种情况下,我们依靠 Java API 来获取线程计数。值得庆幸的是, Thread 类中有一个 activeCount() API:

public class FindNumberofThreads {
    public static void main(String[] args) {
        System.out.println("Number of threads " + Thread.activeCount());
    }
}

输出将是:

Number of threads 2

值得注意的是,如果我们查看 Java VisualVM 中的线程数,我们将看到同一应用程序有更多线程。 这是因为 activeCount() 仅返回同一 ThreadGroup 中的线程数 Java将所有线程分成组以便于管理。

在此示例中,我们只有父 ThreadGroup,main:

public static void main(String[] args) {
    System.out.println("Current Thread Group - " + Thread.currentThread().getThreadGroup().getName());
}
Current Thread Group - main

如果 Java 应用程序中有许多线程组,则 activeCount() 将不会给出正确的输出。例如,它不会返回 GC 线程数。

在这种情况下,我们可以使用 JMX API

public static void main(String[] args) {
    System.out.println("Total Number of threads " + ManagementFactory.getThreadMXBean().getThreadCount());
}

此 API 返回所有线程组、GC、JMX 等的线程总数:

Total Number of threads 6

事实上,JMX 图形工具(例如 Java VisualVM)对其数据使用相同的 API。

4. 命令行工具

之前,我们讨论了 Java VisualVM,这是一种用于分析应用程序中活动线程的图形工具。尽管它是线程实时可视化的绝佳工具,但它对应用程序性能的影响较小。 因此不建议在生产环境中使用

此外,正如我们所讨论的,Java VisualVM 需要 Linux 中的远程连接。事实上,在某些情况下,它需要额外的配置。例如,在 Docker 或 Kubernetes 内运行的应用程序需要额外的服务和端口配置。

在这种情况下,我们必须依靠主机环境中的命令行工具来获取线程数。

幸运的是,Java 提供了很少的命令来获取线程转储 。我们可以将线程转储作为文本文件进行分析,或者使用线程转储分析器工具来检查线程的数量及其状态。

阿里巴巴 Arthas是另一个出色的命令行工具,不需要远程连接或任何特殊配置。

此外,我们还可以通过一些 Linux 命令获取有关线程的信息。 例如,我们可以使用top命令来显示任何 Java 应用程序的所有线程

top -H -p 1

这里, -H 是一个命令行选项,用于显示 Java 进程中的每个线程。如果没有此标志, top 命令将显示进程中所有线程的组合统计信息。 -p 选项按目标应用程序的进程 ID 过滤输出:

top - 15:59:44 up 7 days, 19:23,  0 users,  load average: 0.52, 0.41, 0.36
Threads:  37 total,   0 running,  37 sleeping,   0 stopped,   0 zombie
%Cpu(s):  3.2 us,  2.2 sy,  0.0 ni, 93.4 id,  0.8 wa,  0.0 hi,  0.3 si,  0.0 st
MiB Mem :   1989.2 total,    110.2 free,   1183.1 used,    695.8 buff/cache
MiB Swap:   1024.0 total,    993.0 free,     31.0 used.    838.8 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
   1 flink     20   0 2612160 304084  29784 S   0.0  14.9   0:00.07 java
  275 flink     20   0 2612160 304084  29784 S   0.0  14.9   0:02.87 java
  276 flink     20   0 2612160 304084  29784 S   0.0  14.9   0:00.37 VM Thread
  277 flink     20   0 2612160 304084  29784 S   0.0  14.9   0:00.00 Reference Handl
  278 flink     20   0 2612160 304084  29784 S   0.0  14.9   0:00.00 Finalizer
  279 flink     20   0 2612160 304084  29784 S   0.0  14.9   0:00.00 Signal Dispatch

如上所示,它显示了线程 ID,即 PID 以及每个线程的 CPU 和内存利用率。与 Java VisualVM 类似,top 命令将列出所有线程,包括 GC、JMX 或任何其他子进程。

要查找我们在上述命令中用作参数的进程 ID,我们可以使用 ps 命令:

ps -ef | grep java

事实上,我们也可以使用 ps 命令来列出线程:

ps -e -T | grep 1

-T 选项告诉 ps 命令列出应用程序启动的所有线程:

1     1 ?        00:00:00 java
1   275 ?        00:00:02 java
1   276 ?        00:00:00 VM Thread
1   277 ?        00:00:00 Reference Handl
1   278 ?        00:00:00 Finalizer
1   279 ?        00:00:00 Signal Dispatch
1   280 ?        00:00:03 C2 CompilerThre
1   281 ?        00:00:01 C1 CompilerThre

这里,第一列是 PID,第二列显示每个线程的 Linux 线程 ID。

5. 结论

在本文中,我们看到有多种方法可以找到 Java 应用程序中的线程数。 在大多数情况下,使用命令行选项(如 topps 命令)应该是首选方法

然而,在某些情况下,我们可能还需要像Java VisualVM这样的图形工具。所有代码示例均可在 GitHub 上获取。