死锁的四个必要条件:互斥、持有并等待、不可抢占、循环等待。
死锁场景是两个线程各自持有某个锁,并试图获取对方持有的锁,导致互相等待。
创建死锁示例代码
package io.renren.controller;import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;/*** 通过jstack分析线程死锁场景*/
@RestController
public class DeadlockController {// 定义两个互斥锁对象private final Object lockA = new Object();private final Object lockB = new Object();@GetMapping("/deadlock")public String triggerDeadlock() {new Thread(() -> {synchronized (lockA) {System.out.println("Thread1 acquired lockA");try { Thread.sleep(100); }catch (InterruptedException e) {}synchronized (lockB) {System.out.println("Thread1 acquired lockB");}}}, "Deadlock-Thread-1").start();new Thread(() -> {synchronized (lockB) {System.out.println("Thread2 acquired lockB");try { Thread.sleep(100); }catch (InterruptedException e) {}synchronized (lockA) {System.out.println("Thread2 acquired lockA");}}}, "Deadlock-Thread-2").start();return "死锁已触发,检查控制台日志和线程状态";}}
访问端点触发死锁
http://localhost:8080/deadlock
使用 jstack 分析
查找Java进程PID
jps -l
# 输出示例:
# 12345 com.example.DeadlockDemoApplication
生成线程转储
jstack -l 12345 > thread_dump.txt
分析线程转储:
死锁分析图解
解决死锁的方案
方案1:统一锁获取顺序
// 修改第二个线程的锁获取顺序
new Thread(() -> {synchronized (lockA) { // 改为先获取lockASystem.out.println("Thread2 acquired lockA");try { Thread.sleep(100); } catch (InterruptedException e) {}synchronized (lockB) {System.out.println("Thread2 acquired lockB");}}
}, "Safe-Thread-2").start();
方案2:使用 tryLock 超时机制
//定义成员变量
private final ReentrantLock lock1 = new ReentrantLock();
private final ReentrantLock lock2 = new ReentrantLock();//在函数内部执行
new Thread(() -> {try {if (lock1.tryLock(1, TimeUnit.SECONDS)) {try {System.out.println("Thread1 acquired lock1");Thread.sleep(100);if (lock2.tryLock(1, TimeUnit.SECONDS)) {try {System.out.println("Thread1 acquired lock2");} finally {lock2.unlock();}}} finally {lock1.unlock();}}} catch (InterruptedException e) {e.printStackTrace();}
}).start();
预防死锁的最佳实践
锁顺序:统一所有线程的锁获取顺序
超时机制:使用 tryLock() 替代内置锁
减少锁粒度:避免在方法级别使用 synchronized
静态分析工具:使用 FindBugs/SpotBugs 检测潜在死锁
压力测试:使用 JMeter 模拟高并发场景
最后
实际开发中建议使用 ReentrantLock 等更灵活的工具替代 synchronized,并配合 Arthas 等在线诊断工具进行实时分析。
------------------------------------------------- jstack补充 -----------------------------------------------------
jstack命令用于打印指定Java进程、核心文件或远程调试服务器的Java线程的Java堆栈跟踪信息。
jstack命令可以生成JVM当前时刻的线程快照。线程快照是当前JVM内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。
如果java程序崩溃生成core文件,jstack工具可以用来获得core文件的java stack和native stack的信息,从而可以轻松地知道java程序是如何崩溃和在程序何处发生问题。
当指定的进程在64位Java虚拟机上运行时,可能需要指定-J-d64选项,例如:jstack -J-d64 -m pid。
该命令可能在未来的版本中不可用!!!