在使用多线程时,如果使用不当,就会产生一些问题,比如:
- 死锁导致请求无法响应
- CPU占用率很高,响应很慢
先创建一个springboot应用,模拟一个死锁问题和cpu占用率过高的问题,核心代码如下:
/**
* 模拟一个死锁问题和cpu占用率过高的问题
*/
@RestController
public class ThreadController
{
@GetMapping("/loop")
public String dumpWhile()
{
new Thread(new WhileThread()).start();
return "OK";
}
@GetMapping("/dead")
public String dumpDeadlock()
{
Thread a = new ThreadRunA();
Thread b = new ThreadRunB();
a.start();
b.start();
return "OK";
}
}
class WhileThread implements Runnable{
@Override
public void run()
{
while (true)
{
System.out.println("Thread");
}
}
}
class ThreadRunA extends Thread{
@Override
public void run()
{
System.out.println("======================A====================");
synchronized (A.A){
System.out.println("我要开始执行任务A..."+Thread.currentThread().getName());
try
{
Thread.sleep(5000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
synchronized (B.B)
{
}
System.out.println("我在执行任务结束了A..."+Thread.currentThread().getName()+":"+B.B.hashCode()+":"+A.A.hashCode());
}
}
}
class ThreadRunB extends Thread{
@Override
public void run()
{
System.out.println("======================B====================");
synchronized (B.B){
System.out.println("我要开始执行任务B..."+Thread.currentThread().getName());
try
{
Thread.sleep(1000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
synchronized (A.A)
{
}
System.out.println("我在执行任务结束了B..."+Thread.currentThread().getName()+":"+B.B+":"+A.A);
}
}
}
死锁导致请求无法响应
将上面的这段运行程序,使用如下命令制造死锁现场:
curl http://127.0.0.1:8080/dead
对于死锁问题的排查,具体操作步骤如下:
第一步,通过 jps 命令,查看Java进程的pid.
第二步,通过jstack <pid>命令查看线程dump日志.
当发现死锁时,可以在打印的dump日志中找到Found one Java-level deadlock: 信息,根据信息内容分析问题出现的原因.
jstack 是Java虚拟机自带的一种堆栈跟踪工具,它主要用于打印指定Java进行ID中当前时刻的线程dump日志,线程快照是当前Java虚拟机内每一条线程正在执行的方法堆栈的集合. 生成线程快照的主要目的是定位线程出现长时间停顿的原因, 如线程间死锁,死循环,请求外部的资源导致的长时间等待等,语法格式如下:
CPU占用率过高,响应很慢
为了演示CPU过高的场景,先执行如下的命令, 这个命令会让线程进入死循环状态.
curl http://127.0.0.1:8080/loop
上述命令运行完之后,通过top -c 命令可以动态显示进程及其占用资源的排行榜,从结果可以找到占用CPU最高的进程PID.
可以看到,CPU占用率比较高 的PID是21602,定位到该进程之后,我们再从线程的dump日志中去定位.
- 使用top -H -p 21602 命令查找到该进程中消耗CPU最多的线程,从下面的打印记结果发现PID=29718的线程CPU占用率最高.
- 通过printf "0x%x\n" 29718 命令把对应的线程PID转化为16进制进行打印,之所以要做这步操作是因为线程dump日志中是以16进制来显示线程PID的.
- 执行jstack 命令,打印PID=21602 进程的线程dump日志,然后通过管道命令grep从线程dump日志中查找到CPU占用率最高的线程.
jstack 21602 | grep 0x7426 -A 20 // jstack pid(进程id) | grep 16进制线程 -A 行数
-A 表示显示匹配行及其后面的n行.
通过上述操作后,得到的线程dump信息如下,从该信息中我们发现,在WhileThread.run()方法中因为某个操作导致cpu占用率高,于是基于这个信息我们可以进行分析从而解决该问题.
上述代码内容其实可以分为三个部分.
- 线程的基本信息: "Thread-2”#29 daemon prio=5 os_prio=0 tid-0x00007f84500ce nid=0x13ce2 runnable[0x00007f84a78f7000].
。 Thread-2 表示线程名字,为了更好地辨别,建议大家在使用线程的时候自己进行命令.
。 #29,线程的编号。
。 daemon,表示守护线程。
。 prio,线程的优先级,Java 中的线程优先级分为 1~10 个级别,数字越高表示优先级高,优先级高的线程能够有更高的概率优先得 到 CPU 的执行。
。 os_prio,表示操作系统层面的线程优先级,Java 中配置的线程优先级最终会映射到操系统中的线程优先级。
。 tid=0x00007f84500ce000,JM 内部线程 ID
。 nid,系统线程ID
。 runnable,线程状态
- 线程的运行状态,iava.lang.Thread.State: RUNNABLE.
- 线程的堆栈信息,该信息可以用来快速定位具体的执行命令.