死锁是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种僵局,无一个进程或线程能够继续执行。在Java中,死锁通常发生在多线程环境中,当两个或多个线程相互等待对方释放资源时,就会发生死锁。
定位死锁
定位死锁最常见的方式就是利用 jstack 等工具获取线程栈,然后定位互相之间的依赖关系,进而找到死锁。如果是比较明显的死锁,往往 jstack 等就能直接定位,类似 JConsole 甚至可以在图形界面进行有限的死锁检测。
如果程序运行时发生了死锁,绝大多数情况下都是无法在线解决的,只能重启、修正程序本身问题。所以,代码开发阶段互相审查,或者利用工具进行预防性排查,往往也是很重要的。
死锁例子
public class DeadLockSample{
private static Object lockA = new Object();
private static Object lockB = new Object();
public static void main(String[] args) {
// 启动线程 1
Thread thread1 = new Thread(new Runnable() {
public void run() {
synchronized (lockA) {
System.out.println("Thread 1: Holding lock A");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 1: Waiting for lock B");
synchronized (lockB) {
System.out.println("Thread 1: Holding locks A and B");
}
}
}
},"死锁线程1");
thread1.start();
// 启动线程 2
Thread thread2 = new Thread(new Runnable() {
public void run() {
synchronized (lockB) {
System.out.println("Thread 2: Holding lock B");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 2: Waiting for lock A");
synchronized (lockA) {
System.out.println("Thread 2: Holding locks A and B");
}
}
}
},"死锁线程2");
thread2.start();
}
}
使用jstack定位死锁
无论是Linux系统还是Windows系统都进入到jdk的安装目录bin文件夹下:
第一步:使用 jps -l 指令定位死锁进程
第二步:调用 jstack 获取线程栈(指令:jstack <pid>)
使用Jconsole定位死锁
windows系统
在jdk安装路径bin文件下点击 jconsole.exe
选择对应进程
Linux系统
首先先设置远程连接,先进入jdk路径下设置
cd /usr/local/jdk1.8.0_201/jre/lib/management
management目录下储存着登录用户文件,即远程连接弹窗的用户名和口令对应需要填写的信息。按以下命令操作即可。
mv jmxremote.password.template jmxremote.password
chmod +w jmxremote.password
vim jmxremote.password
放开最后两行注释,monitorRole和controlRole即为用户名,后面为对应的口令。二者皆可用。
修改完password文件后,恢复其编辑权限
chmod 0400 jmxremote.password
启动程序
nohup java -jar -Djava.rmi.server.hostname= 服务器ip地址 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=8069 -Dcom.sun.management.jmxremote.rmi.port=8069 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false ${APP_NAME} --spring.profiles.active=dev --server.port=${PORT} >${LOG} &
到此jconsole配置完成,远程连接,后续检测死锁和windows操作差不多
ps:服务器:关闭防火墙,开放端口号
解决死锁的常见方法包括:
避免嵌套锁
尽量不在持有一个锁的情况下去申请另一个锁。
锁顺序统一
当你需要锁多个对象时,确保你每次都以相同的顺序来锁定它们。查看一下例子
public class DeadLockSample1 {
private static Object lockA = new Object();
private static Object lockB = new Object();
public static void main(String[] args) {
// 启动线程 1
Thread thread1 = new Thread(new Runnable() {
public void run() {
synchronized (lockA) {
System.out.println("Thread 1: Holding lock A");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 1: Waiting for lock B");
synchronized (lockB) {
System.out.println("Thread 1: Holding locks A and B");
}
}
}
},"死锁线程1");
thread1.start();
// 启动线程 2
Thread thread2 = new Thread(new Runnable() {
public void run() {
synchronized (lockA) {
System.out.println("Thread 2: Holding lock A");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread 2: Waiting for lock A");
synchronized (lockB) {
System.out.println("Thread 2: Holding locks A and B");
}
}
}
},"死锁线程2");
thread2.start();
}
}
使用定时锁
Lock
接口的 tryLock(long timeout, TimeUnit unit)
方法可以尝试获取锁,如果在指定时间内未能获取,则会放弃并释放已获得的锁。
ReentrantLock.lockInterruptibly允许在等待时由其它线程调用等待线程的Thread.interrupt方法来中断等待线程的等待而直接返回,这时不用获取锁,而会抛出一个InterruptedException。而ReentrantLock.lock方法不允许Thread.interrupt中断,即使检测到Thread.isInterrupted,一样会继续尝试获取锁,失败则继续休眠。只是在最后获取锁成功后再把当前线程置为interrupted状态。
使用并发工具类
最后,Java并发API提供了一些高级工具,比如java.util.concurrent包中的类,可以帮助咱们更好地管理锁和避免死锁。例如,Semaphore可以用来控制对资源的并发访问数,而CountDownLatch和CyclicBarrier可以用于线程间的同步。
基于JMX获取线程信息(用ThreadMXBean来抓线程死锁信息 )
这是我们又执行了checkDeadLock()方法,来检查JVM中是否有死锁,如果有死锁,则把发生死锁的线程执行interrupt()方法,使该线程响应中断,从而避免发生死锁。(实际应用中,检查死锁可以单独开启一个daemon线程,每间隔一段时间检查一下是否发生死锁,如果有则预警、记录日志、或中断该线程避免死锁)
//基于JMX获取线程信息
public static void checkDeadLock() {
//获取Thread的MBean
ThreadMXBean mbean = ManagementFactory.getThreadMXBean();
//查找发生死锁的线程,返回线程id的数组
long[] deadLockThreadIds = mbean.findDeadlockedThreads();
System.out.println("---" + deadLockThreadIds);
if (deadLockThreadIds != null) {
//获取发生死锁的线程信息
ThreadInfo[] threadInfos = mbean.getThreadInfo(deadLockThreadIds);
//获取JVM中所有的线程信息
Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
for (Entry<Thread, StackTraceElement[]> entry : map.entrySet()) {
for (int i = 0; i < threadInfos.length; i++) {
Thread t = entry.getKey();
if (t.getId() == threadInfos[i].getThreadId()) {
//中断发生死锁的线程
t.interrupt();
//打印堆栈信息
// for (StackTraceElement ste : entry.getValue()) {
// // System.err.println("t" + ste.toString().trim());
// }
}
}
}
}
}
排查CPU占用大的方案:
可以通过linux下top命令查看cpu使用率较高的java进程,进而用top -Hp ➕pid查看该java进程下cpu使用率较高的线程。再用jstack命令查看线程具体调用情况,排查问题。
参考:
jconsole工具监测jvm_linux 运行jconsole本地监控-CSDN博客
Jconsole 开启远程连接遇到的一些坑_jconsole invalid process id-CSDN博客
基于ReentrantLock发生死锁的解决方案-腾讯云开发者社区-腾讯云
https://baijiahao.baidu.com/s?id=1784244731741485433&wfr=spider&for=pc