死循环导致 CPU 彪高问题

代码实现死循环

  1. package com.example.monitorjvmdemo;
  2. import org.springframework.web.bind.annotation.RequestMapping;
  3. import org.springframework.web.bind.annotation.RestController;
  4. import java.util.ArrayList;
  5. import java.util.List;
  6. /***
  7. * cpu问题
  8. *
  9. * @author :anin
  10. * @date :Created in 2021/12/7 14:43
  11. */
  12. @RestController
  13. public class CpuController {
  14. /**
  15. * 死循环
  16. */
  17. @RequestMapping("/loop")
  18. public List<Long> loop() {
  19. // 由于提取 json 的方法中有 bug ,边界没有考虑清楚
  20. // 遇到这种错误的 json 串,就会导致死循环
  21. String data = "{\"data\":[{\"partnerid\":]";
  22. return getPartneridsFromJson(data);
  23. }
  24. /**
  25. * 一段模拟业务代码,导致死循环的产生。
  26. *
  27. * @param data
  28. * @return
  29. */
  30. public static List<Long> getPartneridsFromJson(String data) {
  31. //{\"data\":[{\"partnerid\":982,\"count\":\"10000\",\"cityid\":\"11\"},{\"partnerid\":983,\"count\":\"10000\",\"cityid\":\"11\"},{\"partnerid\":984,\"count\":\"10000\",\"cityid\":\"11\"}]}
  32. //上面是正常的数据
  33. List<Long> list = new ArrayList<>(2);
  34. if (data == null || data.length() <= 0) {
  35. return list;
  36. }
  37. int datapos = data.indexOf("data");
  38. if (datapos < 0) {
  39. return list;
  40. }
  41. int leftBracket = data.indexOf("[", datapos);
  42. int rightBracket = data.indexOf("]", datapos);
  43. if (leftBracket < 0 || rightBracket < 0) {
  44. return list;
  45. }
  46. String partners = data.substring(leftBracket + 1, rightBracket);
  47. if (partners.length() <= 0) {
  48. return list;
  49. }
  50. while (partners.length() > 0) {
  51. int idpos = partners.indexOf("partnerid");
  52. if (idpos < 0) {
  53. break;
  54. }
  55. int colonpos = partners.indexOf(":", idpos);
  56. // 在这里会导致死循环,
  57. int commapos = partners.indexOf(",", idpos);
  58. if (colonpos < 0 || commapos < 0) {
  59. //partners = partners.substring(idpos+"partnerid".length());//1
  60. continue;
  61. }
  62. String pid = partners.substring(colonpos + 1, commapos);
  63. if (pid.length() <= 0) {
  64. //partners = partners.substring(idpos+"partnerid".length());//2
  65. continue;
  66. }
  67. try {
  68. list.add(Long.parseLong(pid));
  69. } catch (Exception e) {
  70. //do nothing
  71. }
  72. partners = partners.substring(commapos);
  73. }
  74. return list;
  75. }
  76. }

访问

访问 /loop 服务,就会进入死循环

  1. ###
  2. GET http://localhost:8080/loop

查看cpu占用率

image.png
通过输出信息可以看到,PID 为 13869 这个进程 ID CPU 占用 99.1%

导出文件

  1. anin@anin monitor-jvm-demo % jstack 13869 > 13869.txt

查看进程里的线程

我们还可以通过 top 命令找出指定进程里面的所有 线程

  1. # 可以打印出该进程中的所有线程
  2. $ top -H -p 33225
  3. # 但是 mac 上 top 没有 -H 参数,看不到

image.png
就是如上图所示,不过这个 PID 是 十进制 的,jstack 中打印出来的信息线程 ID 是 十六进制的,也就是 nid=0x2037 这种。
下面可以通过 printf 把十进制转成十六进制

  1. anin@anin ~ % printf "%x\n" 8247
  2. 2037

那么拿到这个线程 ID 去搜索刚刚打印出来的内容

  1. # 这里就假装上面找到的线程 id 和实际是一致的
  2. # 也确实是这个线程导致的死循环
  3. "http-nio-8080-exec-1" #26 daemon prio=5 os_prio=31 tid=0x000000014b95e800 nid=0x9803 runnable [0x000000016f83c000]
  4. java.lang.Thread.State: RUNNABLE
  5. at java.lang.String.indexOf(String.java:1772)
  6. at java.lang.String.indexOf(String.java:1718)
  7. at com.example.monitorjvmdemo.CpuController.getPartneridsFromJson(CpuController.java:59)
  8. at com.example.monitorjvmdemo.CpuController.loop(CpuController.java:25)
  9. at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  10. at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)

首先它的运行状态是 RUNNABLE 的,正在运行中,并且如果你搜索这个 getPartneridsFromJson 的话,会发现有 5 个线程,这里对应的是上面 top 找线程时那个图,排在最前面的最耗 CPU 的线程。这就很容易就定位到了出问题的代码

死锁问题

代码实现死锁

  1. package com.example.monitorjvmdemo;
  2. import org.springframework.web.bind.annotation.RequestMapping;
  3. import org.springframework.web.bind.annotation.RestController;
  4. /**
  5. * 死锁问题
  6. */
  7. @RestController
  8. public class LockController {
  9. private Object lock1 = new Object();
  10. private Object lock2 = new Object();
  11. /**
  12. * 死锁
  13. */
  14. @RequestMapping("/deadlock")
  15. public String deadlock() {
  16. new Thread(() -> {
  17. synchronized (lock1) {
  18. try {
  19. Thread.sleep(1000);
  20. } catch (Exception e) {
  21. }
  22. synchronized (lock2) {
  23. System.out.println("Thread1 over");
  24. }
  25. }
  26. }).start();
  27. new Thread(() -> {
  28. synchronized (lock2) {
  29. try {
  30. Thread.sleep(1000);
  31. } catch (Exception e) {
  32. }
  33. synchronized (lock1) {
  34. System.out.println("Thread2 over");
  35. }
  36. }
  37. }).start();
  38. return "deadlock";
  39. }
  40. }

定义两个线程:

  1. 第一个线程先锁 lock1,休眠 1 秒,再获取 lock2
  2. 另一个线程先锁 lock2,休眠 1 秒,再获取 lock1

也就是说,当线程 1 从睡眠中醒来时,lock2 已经被获取过了,就一直等待,而另一个线程一直等待 lock1 的释放。两个线程就互相等待起来了,造成了死锁

访问

访问 /deadlock 服务后,虽然返回了数据,但是程序中其实已经有一个死锁了。

  1. ###
  2. GET http://localhost:8080/deadlock

分析

这种情况下使用 top 命令查看消耗资源的进程 ID 就不合适了,因为死锁的话,它是等待状态,不怎么耗资源的。

导出文件

  1. anin@anin monitor-jvm-demo % jps
  2. 13810
  3. 14739 Jps
  4. 14692 Launcher
  5. 14693 MonitorJvmDemoApplication
  6. 13838 RemoteMavenServer36
  7. anin@anin monitor-jvm-demo % jstack 14693 > 14693.txt

打开文件,直接拉到最后
image.png
Found one Java-level deadlock: 直接提示发现一个 java 级死锁,并且有相关的 锁互相持有说明,还有相关的堆栈信息。
由于该场景是我们主动模拟的,比较容易找到,真实的线上场景代码很复杂,可能没有这么容易找出来,但是有了这样的工具,还是比较容易定位到的。