Java 后端 CPU 飙高到 100%?快速定位线程阻塞与死循环问题的方法
各位朋友,大家好!今天我们来聊聊一个让 Java 后端工程师头疼的问题:CPU 飙高到 100%。 这个问题往往意味着我们的服务出现了性能瓶颈,严重时会导致服务崩溃。 面对这种情况,我们需要冷静分析,快速定位问题根源。 今天我将分享一些常用的方法,帮助大家快速诊断线程阻塞和死循环导致的 CPU 飙高问题。
一、问题现象与初步判断
首先,我们需要确认 CPU 的确飙高了。 可以通过以下方式观察:
- Linux 系统: 使用 
top或htop命令,观察 CPU 使用率最高的进程。 - Windows 系统: 使用任务管理器,查看 CPU 占用率最高的进程。
 - 监控系统: 如果使用了监控系统(如 Prometheus + Grafana),可以查看 CPU 使用率的监控指标。
 
如果确认是 Java 进程 CPU 占用率过高,那么接下来需要判断是所有线程都在高负荷运行,还是少数线程导致的 CPU 飙高。 这将影响我们后续的排查方向。
- 所有线程高负荷: 这通常意味着整体系统负载过高,或者代码存在普遍的性能问题,例如大量的计算密集型操作、频繁的 GC 等。
 - 少数线程高负荷: 这很可能是由于线程阻塞或死循环导致的。
 
二、定位高 CPU 占用线程
定位到高 CPU 占用线程是关键的第一步。 我们需要借助一些工具来完成这个任务。
- 
jstack 命令:
jstack是 JDK 自带的线程堆栈分析工具,它可以打印出 Java 进程中所有线程的堆栈信息。 通过分析堆栈信息,我们可以找到正在执行的线程,以及它们正在执行的代码。- 
获取进程 ID (PID): 使用
jps命令或者ps -ef | grep java命令找到 Java 进程的 PID。jps -l输出类似:
12345 com.example.MyApplication这里
12345就是进程 ID。 - 
使用 jstack 命令: 使用以下命令打印线程堆栈信息。
jstack 12345 > thread_dump.txt这将把线程堆栈信息输出到
thread_dump.txt文件中。 
 - 
 - 
VisualVM: VisualVM 是一个功能强大的 Java 虚拟机监控工具,它提供了图形化的界面,可以方便地查看线程信息、内存信息、CPU 使用率等。VisualVM 默认包含在 JDK 中。
- 启动 VisualVM (
jvisualvm命令)。 - 在 VisualVM 中连接到正在运行的 Java 进程。
 - 切换到 "Threads" (线程) 选项卡。
 - 按照 CPU 时间排序,找到 CPU 时间最高的线程。
 
 - 启动 VisualVM (
 - 
Arthas: Arthas 是阿里巴巴开源的一款 Java 诊断工具,它提供了丰富的命令,可以方便地进行各种诊断操作,包括查看线程信息、方法调用堆栈、类加载信息等。
- 下载并安装 Arthas。
 - 启动 Arthas 并连接到 Java 进程。
 - 
使用
thread -n 3命令,查看 CPU 使用率最高的 3 个线程。thread -n 3Arthas 会输出类似如下信息:
"pool-1-thread-1" Id=10 cpuUsage=98% deltaCpuUsage=97% RUNNABLE at com.example.MyTask.run(MyTask.java:20) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748)从 Arthas 的输出中,我们可以看到线程 ID、线程名称、CPU 使用率,以及线程的堆栈信息。
 
 
三、分析线程堆栈信息
获取到线程堆栈信息后,我们需要仔细分析它,才能找到问题的根源。 线程堆栈信息包含了线程的执行路径,以及线程当前的状态。
- 
线程状态: 线程堆栈信息中会包含线程的状态,常见的状态包括:
- RUNNABLE: 线程正在执行,或者可以被执行。如果一个线程长时间处于 RUNNABLE 状态,并且 CPU 使用率很高,那么很可能存在死循环。
 - BLOCKED: 线程被阻塞,等待获取锁。 如果多个线程同时竞争同一个锁,并且其中一个线程长时间持有锁不释放,那么其他线程就会被阻塞。
 - WAITING: 线程正在等待其他线程的通知。 例如,线程调用了 
Object.wait()方法,或者Thread.join()方法。 - TIMED_WAITING: 线程正在等待一段时间后自动唤醒。 例如,线程调用了 
Thread.sleep()方法,或者Object.wait(long timeout)方法。 
 - 
代码位置: 线程堆栈信息中会包含代码位置,也就是线程正在执行的代码行号。 通过代码位置,我们可以找到导致 CPU 飙高的具体代码。
 - 
锁信息: 如果线程处于 BLOCKED 状态,线程堆栈信息中会包含锁的信息,包括锁的持有者线程 ID,以及等待获取锁的线程 ID。
 
四、常见问题与解决方案
- 
死循环: 死循环是最常见的导致 CPU 飙高的问题之一。 如果一个线程长时间处于 RUNNABLE 状态,并且 CPU 使用率很高,那么很可能存在死循环。
- 
识别死循环: 查看线程堆栈信息,找到长时间执行的代码块。 检查循环条件是否正确,以及循环体内部是否存在逻辑错误。
 - 
示例代码:
public class DeadLoop { public static void main(String[] args) { int i = 0; while (true) { // 死循环 i++; } } } - 
解决方法: 修改循环条件,确保循环能够正常结束。 或者使用
break语句跳出循环。 
 - 
 - 
锁竞争: 如果多个线程同时竞争同一个锁,并且其中一个线程长时间持有锁不释放,那么其他线程就会被阻塞,导致 CPU 飙高。
- 
识别锁竞争: 查看线程堆栈信息,找到处于 BLOCKED 状态的线程。 确定这些线程正在等待获取哪个锁,以及哪个线程持有这个锁。
 - 
示例代码:
public class LockContention { private static final Object lock = new Object(); public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(() -> { synchronized (lock) { System.out.println("Thread 1 acquired the lock"); try { Thread.sleep(5000); // 模拟长时间持有锁 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Thread 1 released the lock"); } }); Thread thread2 = new Thread(() -> { synchronized (lock) { System.out.println("Thread 2 acquired the lock"); } }); thread1.start(); Thread.sleep(100); // 确保 thread1 先获取锁 thread2.start(); } } - 
解决方法:
- 减少锁的持有时间: 尽可能减少锁的持有时间,避免长时间阻塞其他线程。
 - 使用更细粒度的锁: 将一个大的锁拆分成多个小的锁,降低锁竞争的概率。
 - 使用读写锁: 如果读操作远多于写操作,可以使用读写锁,允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。
 - 使用无锁数据结构: 使用无锁数据结构(例如 ConcurrentHashMap、AtomicInteger)可以避免锁竞争,提高并发性能。
 
 
 - 
 - 
线程池耗尽: 如果线程池中的线程全部被占用,新的任务将无法执行,导致请求堆积,CPU 飙高。
- 
识别线程池耗尽: 查看线程池的监控指标,例如活跃线程数、队列长度、拒绝任务数等。 如果活跃线程数达到最大线程数,并且队列长度持续增加,那么很可能存在线程池耗尽的问题。
 - 
示例代码:
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadPoolExhaustion { public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(2); // 线程池大小为 2 for (int i = 0; i < 10; i++) { final int taskNumber = i; executor.submit(() -> { System.out.println("Task " + taskNumber + " started"); try { Thread.sleep(2000); // 模拟耗时任务 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Task " + taskNumber + " finished"); }); } executor.shutdown(); } } - 
解决方法:
- 增加线程池大小: 根据实际需要,增加线程池的最大线程数。
 - 使用有界队列: 使用有界队列可以防止任务无限堆积,避免 OOM 错误。
 - 优化任务执行时间: 缩短单个任务的执行时间,提高线程池的吞吐量。
 - 使用拒绝策略:  当线程池中的线程全部被占用,并且队列已满时,可以使用拒绝策略来处理新的任务。 常见的拒绝策略包括:
AbortPolicy: 直接抛出 RejectedExecutionException 异常。CallerRunsPolicy: 由提交任务的线程执行该任务。DiscardPolicy: 直接丢弃该任务。DiscardOldestPolicy: 丢弃队列中最老的任务,然后尝试执行该任务。
 
 
 - 
 - 
频繁的 GC: 频繁的 GC 会导致 JVM 暂停所有线程,影响系统性能,甚至导致 CPU 飙高。
- 识别频繁的 GC:  使用 
jstat -gcutil <pid> 1000命令,观察 GC 的频率和耗时。 如果 GC 的频率很高,并且耗时很长,那么很可能存在频繁的 GC 问题。 - 解决方法:
- 优化代码,减少对象创建: 避免创建不必要的对象,减少内存占用。
 - 调整 JVM 参数: 调整 JVM 的堆大小、新生代大小、老年代大小等参数,优化 GC 性能。
 - 使用合适的 GC 算法: 根据应用场景选择合适的 GC 算法。 例如,CMS 算法适合于对停顿时间要求较高的应用,G1 算法适合于大堆内存的应用。
 
 
 - 识别频繁的 GC:  使用 
 
五、排查步骤总结
为了更清晰地总结排查过程,我将步骤整理成表格:
| 步骤 | 操作 | 工具/命令 | 说明 | 
|---|---|---|---|
| 1 | 确认 CPU 飙高 | top / htop / 任务管理器 / 监控系统 | 
确认 Java 进程 CPU 占用率过高 | 
| 2 | 获取线程堆栈信息 | jstack <pid> > thread_dump.txt / VisualVM / Arthas | 
获取 Java 进程的线程堆栈信息 | 
| 3 | 找到高 CPU 占用线程 | 分析 thread_dump.txt / VisualVM / thread -n <num> (Arthas) | 
找到 CPU 使用率最高的线程 | 
| 4 | 分析线程堆栈信息 | 文本编辑器 / VisualVM / Arthas | 重点关注线程状态 (RUNNABLE, BLOCKED, WAITING, TIMED_WAITING),代码位置,锁信息 | 
| 5 | 根据线程堆栈信息,定位问题根源 | 常见问题包括死循环、锁竞争、线程池耗尽、频繁的 GC 等 | |
| 6 | 解决问题 | 根据具体问题,采取相应的解决方案 | 
六、预防措施与最佳实践
除了解决已经出现的问题,我们更应该注重预防,从源头上避免 CPU 飙高问题的发生。
- 代码审查: 进行代码审查,检查代码是否存在潜在的性能问题,例如死循环、锁竞争、资源泄漏等。
 - 性能测试: 在上线前进行性能测试,模拟高并发场景,发现潜在的性能瓶颈。
 - 监控系统: 建立完善的监控系统,监控 CPU 使用率、内存使用率、线程池状态、GC 频率等关键指标,及时发现异常情况。
 - 日志记录: 记录详细的日志信息,方便问题排查。
 - 使用成熟的框架和库: 使用经过充分测试和优化的框架和库,可以减少潜在的性能问题。
 - 遵循最佳实践: 遵循 Java 编码最佳实践,编写高质量的代码。
 
最后,一些提示
- 不要盲目猜测: 不要盲目猜测问题的原因,要根据实际数据进行分析。
 - 逐步排查: 从最简单的开始排查,逐步缩小问题范围。
 - 善用工具: 善用各种诊断工具,例如 jstack, VisualVM, Arthas 等,可以提高问题排查效率。
 - 持续学习: 不断学习新的技术和工具,提高自己的问题解决能力。
 
希望今天的分享能帮助大家更好地定位和解决 Java 后端 CPU 飙高问题。 谢谢大家!
问题的解决在于细致的分析和有效的工具使用
要解决CPU飙高问题,需要细致地分析线程堆栈信息,并熟练运用各种诊断工具。
预防措施是避免问题的关键
通过代码审查、性能测试和完善的监控系统,可以有效预防CPU飙高问题的发生。