1 概述

1.1 背景介绍

鲲鹏DevKit Java性能分析工具是一款针对鲲鹏平台的性能分析和调优工具,Java性能分析工具是针对基于鲲鹏的服务器上运行的Java程序的性能分析和优化工具,能图形化显示Java程序的堆、线程、锁、垃圾回收等信息,收集热点函数、定位程序瓶颈点,帮助用户采取针对性优。

1.2 适用对象

  • 企业
  • 个人开发者
  • 高校学生

1.3 案例时间

本案例总时长预计40分钟。

1.4 案例流程

611ed0c36cfebb945599aa3a74b5a98b.png

说明:

① 自动部署鲲鹏服务器;
② 安装鲲鹏Devkit插件;
③ 配置在线分析环境,通过浏览器访问鲲鹏服务器,添加目标节点;
④ 执行、编译死锁代码;
⑤ 死锁在线分析;
⑥ 修改代码,编译执行;
⑦ 在线分析。

1.5 资源总览

本案例预计花费总计0元。

资源名称 规格 单价(元) 时长(h)
鲲鹏沙箱资源 ECS/2核4GEIP/独享3M带宽EVS/通用型SSD 40G 免费 1
云主机 2vCPUs | 4GB 免费 1

利用鲲鹏DevKit剖析Java死锁问题的性能分析👈👈👈👈完整版 案例体验请点这里进行查看。

2 操作步骤

2.1 自动部署鲲鹏服务器

在云主机桌面右键选择“Open Terminal Here”,打开命令终端窗口。

4bfeee0b27092ca3d1c357b3543c6464.png

执行自动部署命令如下:

hcd deploy --password 远端服务器密码 --time 3600
# --password    待部署项目所在ECS的root用户密码(至少8个字符)
# --time value   待部署资源的保留期(单位为秒,至少600秒,默认600秒)。当前案例预估需要40分钟,可以配置time为1小时保留期。

该命令会自动部署鲲鹏服务器。首次部署会直接执行,旧资源未到期时重复部署,会提示是否删除前面创建的资源,可以删除旧资源再次部署。

aeec95dc9eaf0cc5661cdad6bff1cb77.png

可以看到鲲鹏服务器链接:https://113.44.69.106:8084,表示部署成功,记录部署远端服务器公网IP,如截图中对应的就是:113.44.69.106 。

af16cba77eba4f05bf1af9aff8bb46b6.png

在开发者空间,我的开发资源下,找到鲲鹏沙箱环境,点击“使用详情”,可以看到我们刚刚自动部署的鲲鹏服务器,创建时间及资源释放时间等信息。

39c62438d50f031f4d971ca0b83b9db5.png

2.2 安装鲲鹏Devkit插件

云主机桌面单击鼠标右键,在菜单中选择“Open Terminal Here”打开终端。

a4553949d8fce310a65cd38f6797e77a.png

通过ssh连接云服务器,如果有yes/no选择输入“yes”,然后输入 “云主机密码”,出现“Welcome to XXX”代表连接成功。

ssh root@云主机IP

880ad3126cc55605a11df2c9f34983e1.png

输入地址“wget https://kunpeng-repoXXX”下载鲲鹏DevKit压缩包:

wget https://kunpeng-repo.obs.cn-north-4.myhuaweicloud.com/Kunpeng%20DevKit/Kunpeng%20DevKit%2024.0.RC3/DevKit-All-24.0.RC3-Linux-Kunpeng.tar.gz

ce0acb498478d2437f3fefcf2dcc6935.png

稍作等待,下载完成执行“ls”可以看到下载的压缩包。

1b0e650adecf47cdaff960836dc0e15a.png

解压文件.

tar -zvxf DevKit-All-24.0.RC3-Linux-Kunpeng.tar.gz

6f2ca41800b71cb87789209780705099.png

进入文件夹,可以看到“install.sh”,执行安装。

cd DevKit-All-24.0.RC3-Linux-Kunpeng
sudo ./install.sh

22f9a65582be9119ae09e0c1f2a7a956.png

如下图步骤,第一个默认回车即可。

998adf136258977301b179b33e444102.png

出现如下图所示时选择“2”,去选择插件(1是安装全部,如选错可以“ctrl + c”停止,重复上面 “sudo ./install.sh”指令即可)。

d4439bd8d08d4a906ab9d6c5a6e5b404.png

本案例只用到java性能分析,选择“6”,后面选项根据提示操作,多数默认回车即可,直至安装成功。

e3c953922e72a811cffea0618d40d1ac.png

安装成功可以看到“https://192.168.2.191:8086” 。

64c4a7084e7daae77a3e0e4bcf1bef36.png

执行命令,修改端口。

如下命令的第一个ip为2.1自动部署的鲲鹏服务器IP,第二个ip为上图安装成功的IP。

iptables -t nat -A PREROUTING -d XX.XX.XX.XX -p tcp --dport 8086 -j DNAT --to-destination XX.XX.XX.XX:8086

c24653d853ba685a0751cfcff616279c.png

2.3 配置在线分析环境

通过浏览器访问鲲鹏服务器,添加目标节点,以配置在线分析环境。

打开浏览器,输入“https://XX.XX.XX.XX:8086”(IP为2.1自动部署的鲲鹏服务器 IP),如果提示风险,点击接受并继续。

622744541e5c2574d3a1efd80f6aa68c.png

创建密码,并登录。

be30cc8997361f2af773a59381e11fc1.png

登录完成后添加目标环境:点击左上角“调优 添加”,在添加目标环境弹窗中输入:“用户名”,“云服务器密码”,点击“确定”;跳出来弹窗点击“确定”即可。

124033ba847e1fd3179cce702ca508ed.png

2.4 执行编译代码

云主机桌面单击鼠标右键,在菜单中选择“Open Terminal Here”打开终端。

a4553949d8fce310a65cd38f6797e77a.png

通过ssh连接云服务器,如果有yes/no选择输入“yes”,然后输入 “云主机密码”,出现“Welcome to XXX”代表连接成功。

ssh root@云主机IP

进入到云主机后,执行如下命令查看当前云主机的JDK安装情况。

java -version

5d2ee451898c9db2fa2ec8cc19edb570.png

可以看到JDK默认是安装配置好的,输入“vim DeadLock.java”,回车进入,点击“i”进入编辑模式。

vim DeadLock.java

DeadLock.java 代码地址,如跳转不了请输入以下地址复制代码:
https://github.com/kunpengcompute/devkitdemo/blob/main/Hyper_tuner/testdemo/基于java性能分析工具的死锁调优实践/DeadLock.java

DeadLock.java代码如下(因格式可能存在问题建议进入链接进行复制):

public class DeadLock {
	private static final Integer lockOne = new Integer(1);
	private static final Integer lockTwo = new Integer(2);	
	public static void main(String[] args){
		new Thread(()->{
			try{
				System.out.println("thread1 is running");
				synchronized (lockOne){
					System.out.println("thread1 get lock obj1");
					Thread.sleep(1000L);
					synchronized (lockTwo){
					    System.out.println("thread12 get lock obj2");
					    Thread.sleep(1000L);
				    }
				}
			} catch(InterruptedException e){
				e.printStackTrace();
			}
		}).start();
		new Thread(()->{
			try{
				System.out.println("thread2 is running");
				synchronized (lockTwo){
					System.out.println("thread2 get lock obj2");
					Thread.sleep(1000L);
					synchronized (lockOne){
					    System.out.println("thread2 get lock obj1");
					    Thread.sleep(1000L);
				    }
				}
			} catch(InterruptedException e){
				e.printStackTrace();
			}
		}).start();
	}
}

代码复制完毕,键盘点击“esc”,然后输入“:wq”保存退出。

99a289a65e3f2fae0cbbe98cc2bf1e62.png

javac DeadLock.java
java DeadLock

编译java文件,然后执行代码,可以看到执行成功。

af3272cb6287447cf3beb8b857eff05a.png

2.5 死锁在线分析

进入浏览器界面,点击“调优”,点击“root@XXX”,点击DeadLock的“在线分析”。

82bd130ab032861a04a77a86bbdbe8cf.png

对程序进行在线分析,在概览页签下观察每种状态的线程数,发现有两个线程处于阻塞状态,有死锁的嫌疑。7d28178df6cbcd8274dcc077093aa5c0.png

切换到CPU页签下的线程列表,找到阻塞状态的线程。观察一段时间发现发两个线程一直处于阻塞状态。

56fe437b5e6660135aaff6e65368f00e.png

执行多次线程转储操作:在CPU下,点击“执行线程转储”,转储成功后点击“线程转储”下的锁分析图。发现两个阻塞中的线程发生了死锁。

15cfbf6a8a0641f935765d241e7b9266.png

选择CPU页签中线程转储下的原始数据,根据线程转储的原始数据得到死锁的相关信息。

点击优化建议的“查看详情”,查看死锁问题优化建议。

3b05056a048e05fcda4e4b3049d8c178.png

此时,我们已经获取到阻塞的原因及优化建议,点击“停止分析”。

463c28789d53a52952bb094218b1ee69.png

2.6 修改代码,再次编译分析

找到阻塞的原因,接下来参考优化建议进行代码修改,解决死锁问题。

优化点解析说明:

  1. 共享变量的使用及操作

修改后的DeadLock类中定义了一个volatile修饰的共享变量i,并且在每个线程获取锁成功并执行完同步代码块后,会对i进行自增操作(i++)。而修改前的DeadLock类中没有这样的共享变量及相关操作。

private static volatile Integer i = 1;

原理说明:volatile关键字保证可见性

volatile关键字确保了共享变量i在多个线程之间的可见性。当一个线程修改了i的值,其他线程能够立即看到这个修改。这样,线程能够根据i的最新值来正确判断是否进入同步代码块获取锁,保证了整个机制的正确性。如果没有volatile关键字,线程可能会使用i的旧值,导致程序逻辑错误,无法有效避免死锁。

  1. 线程执行条件判断

在修改后的DeadLock类中,每个线程内部有一个while(true)循环,并且根据共享变量i的奇偶性来决定是否获取锁。当i为奇数时,第一个线程尝试获取锁;当i为偶数时,第二个线程尝试获取锁。而修改前DeadLock类中,两个线程启动后直接尝试获取锁,没有基于共享变量的条件判断。

while(true){
          if(i % 2 == 1){
            synchronized (lockOne){
              System.out.println("thread1 get lock obj1");
              Thread.sleep(1000L);
              synchronized (lockTwo){
                  System.out.println("thread12 get lock obj2");
                  Thread.sleep(1000L);
                }
            }
            i++;
          }
        }
while(true){
          if(i % 2 == 0){
            synchronized (lockOne){
              System.out.println("thread1 get lock obj1");
              Thread.sleep(1000L);
              synchronized (lockTwo){
                  System.out.println("thread12 get lock obj2");
                  Thread.sleep(1000L);
                }
            }
            i++;
          }
        }

原理说明:打破循环等待条件

在修改前DeadLock中,线程 1 先获取lockOne锁,然后等待lockTwo锁;而线程 2 先获取lockTwo锁,然后等待lockOne锁,形成了循环等待,导致死锁。

在修改后DeadLock中,通过引入共享变量i和条件判断,使得两个线程不会同时去竞争lockOne和lockTwo锁。例如,当i为奇数时,只有线程 1 会去尝试获取lockOne锁,线程 2 此时不会竞争lockOne锁,从而打破了循环等待的条件。当线程 1 获取锁并执行完同步代码块后,i变为偶数,此时线程 2 才有可能去获取lockOne锁,避免了死锁的发生。

回到终端,先停止程序:ctrl + z ,输入“vim DeadLock.java”,回车进入,点击“i”进入编辑模式。

vim DeadLock.java

修改后代码如下:

public class DeadLock {
	private static final Integer lockOne = new Integer(1);
	private static final Integer lockTwo = new Integer(2);
	private static volatile Integer i = 1;
	public static void main(String[] args){
		new Thread(()->{
			try{
				System.out.println("thread1 is running");
        while(true){
          if(i % 2 == 1){
            synchronized (lockOne){
              System.out.println("thread1 get lock obj1");
              Thread.sleep(1000L);
              synchronized (lockTwo){
                  System.out.println("thread12 get lock obj2");
                  Thread.sleep(1000L);
                }
            }
            i++;
          }
        }
			} catch(InterruptedException e){
				e.printStackTrace();
			}
		}).start();
		new Thread(()->{
			try{
				System.out.println("thread2 is running");
        while(true){
          if(i % 2 == 0){
            synchronized (lockOne){
              System.out.println("thread1 get lock obj1");
              Thread.sleep(1000L);
              synchronized (lockTwo){
                  System.out.println("thread12 get lock obj2");
                  Thread.sleep(1000L);
                }
            }
            i++;
          }
        }
			} catch(InterruptedException e){
				e.printStackTrace();
			}
		}).start();
	}
}

代码变动部分请参考下图,代码修改完毕后,键盘点击“esc”,然后输入“:wq”保存退出。
46203f5246185a5a2d77f62a82c317c1.png

编译java文件,然后执行代码:

javac DeadLock.java  
java DeadLock

可以看到执行成功。运行效果如下:
313c64f2ed585f27d32cf61a284a742b.png
回到浏览器,点击DeadLock 的“在线分析”。

bf49fb72d243f92d0c6c58cde00c6642.png

查看优化后程序,调整两个线程持有锁的顺序后,程序不再发生死锁。

94be0d15a56e3fa3162edd24d5925777.png

可以看到建议中数量少了一条CPU死锁已经不存在,问题已被解决。点击“停止分析”,终端停止代码。

0fc35a28547c04953775311e445fdcb1.png

总结:Java中的死锁问题一旦发生很难定位具体的代码位置,因为程序干扰因素比较多,所以涉及加锁解锁代码逻辑地方一定要仔细。建议如果涉及加锁的代码逻辑,程序是通过新起线程去执行或者是在线程池中执行,一定给线程设置有特定业务逻辑的名称,一旦发生问题也好定位。

在进行其他程序调优时,需要根据鲲鹏DevKit Java性能分析工具采集的实际结果和对应的优化建议进行调优操作。具体的调优思路可以参考本次实践。

至此,利用鲲鹏DevKit剖析Java死锁问题的性能分析实操全部结束。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐