1. 死锁问题概述
死锁是Java多线程编程中最令人头疼的问题之一。当两个或多个线程互相持有对方需要的资源,并且都在等待对方释放资源时,就会形成死锁状态。这种情况会导致程序完全卡死,无法继续执行。
在实际开发中,死锁问题往往出现在以下场景:
- 数据库事务操作
- 文件系统资源竞争
- 分布式锁的使用
- 复杂业务逻辑中的同步控制
2. 死锁产生的必要条件
要理解如何排查死锁,首先需要了解死锁产生的四个必要条件:
2.1 互斥条件
资源一次只能被一个线程占用。比如Java中的synchronized关键字就是实现互斥的典型方式。
2.2 占有且等待
线程已经持有至少一个资源,并且正在等待获取其他被占用的资源。
2.3 非抢占条件
已经分配给线程的资源,不能被其他线程强行夺取,必须由持有线程显式释放。
2.4 循环等待条件
存在一个线程等待的循环链,每个线程都在等待下一个线程所占用的资源。
3. 死锁排查工具
3.1 jstack工具
jstack是JDK自带的线程堆栈分析工具,可以打印出Java进程中所有线程的堆栈信息。
使用方式:
jstack -l <pid>3.2 jconsole工具
jconsole是JDK提供的图形化监控工具,可以查看线程状态和检测死锁。
3.3 VisualVM
VisualVM是一个功能更强大的图形化工具,可以分析线程转储和检测死锁。
4. 死锁排查步骤
4.1 获取线程转储
首先需要获取Java进程的线程转储信息:
jstack -l <pid> > thread_dump.txt4.2 分析线程状态
在thread_dump.txt中查找"BLOCKED"状态的线程,这些线程很可能参与了死锁。
4.3 查找死锁信息
jstack输出的最后部分通常会明确标识出死锁的线程和锁资源:
Found one Java-level deadlock: ============================= "Thread-1": waiting to lock monitor 0x00007f8b3800f0e8 (object 0x000000076ab45c50, a java.lang.Object), which is held by "Thread-0" "Thread-0": waiting to lock monitor 0x00007f8b3800f1e8 (object 0x000000076ab45c60, a java.lang.Object), which is held by "Thread-1"4.4 定位问题代码
根据线程转储中的堆栈信息,定位到具体的代码位置,分析锁的获取顺序。
5. 死锁预防策略
5.1 锁顺序化
确保所有线程以相同的顺序获取锁资源,避免循环等待。
5.2 锁超时机制
使用tryLock()方法设置获取锁的超时时间,避免无限期等待。
5.3 减少锁粒度
尽量减小同步代码块的范围,减少持有锁的时间。
5.4 使用并发工具类
优先考虑使用java.util.concurrent包中的高级并发工具,如ConcurrentHashMap、CountDownLatch等。
6. 实战案例分析
6.1 典型死锁场景
public class DeadlockDemo { private static final Object lock1 = new Object(); private static final Object lock2 = new Object(); public static void main(String[] args) { new Thread(() -> { synchronized (lock1) { try { Thread.sleep(100); } catch (Exception e) {} synchronized (lock2) { System.out.println("Thread1 got both locks"); } } }).start(); new Thread(() -> { synchronized (lock2) { try { Thread.sleep(100); } catch (Exception e) {} synchronized (lock1) { System.out.println("Thread2 got both locks"); } } }).start(); } }6.2 解决方案
修改锁获取顺序,确保两个线程都以相同的顺序获取锁:
public class FixedDeadlockDemo { // ... 相同的锁对象定义 public static void main(String[] args) { new Thread(() -> { synchronized (lock1) { try { Thread.sleep(100); } catch (Exception e) {} synchronized (lock2) { System.out.println("Thread1 got both locks"); } } }).start(); new Thread(() -> { synchronized (lock1) { // 改为先获取lock1 try { Thread.sleep(100); } catch (Exception e) {} synchronized (lock2) { System.out.println("Thread2 got both locks"); } } }).start(); } }7. 高级排查技巧
7.1 使用ThreadMXBean
Java提供了ThreadMXBean接口,可以编程式检测死锁:
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); long[] deadlockedThreads = threadMXBean.findDeadlockedThreads(); if (deadlockedThreads != null) { ThreadInfo[] threadInfos = threadMXBean.getThreadInfo(deadlockedThreads); for (ThreadInfo threadInfo : threadInfos) { System.out.println("Deadlocked thread: " + threadInfo.getThreadName()); System.out.println("Lock owner: " + threadInfo.getLockOwnerName()); System.out.println("Stack trace:"); for (StackTraceElement ste : threadInfo.getStackTrace()) { System.out.println("\t" + ste); } } }7.2 使用Arthas工具
Arthas是阿里开源的Java诊断工具,提供了更强大的死锁检测功能:
# 启动Arthas java -jar arthas-boot.jar # 检测死锁 thread -b8. 数据库死锁排查
8.1 MySQL死锁日志
MySQL可以通过以下命令查看死锁日志:
SHOW ENGINE INNODB STATUS;8.2 分析死锁信息
在输出结果中查找"LATEST DETECTED DEADLOCK"部分,会详细记录死锁涉及的SQL语句和锁资源。
9. 性能优化建议
9.1 减少同步范围
尽量缩小synchronized块的范围,只在必要的地方加锁。
9.2 使用读写锁
对于读多写少的场景,考虑使用ReentrantReadWriteLock替代普通的互斥锁。
9.3 避免锁嵌套
尽量避免在一个同步块内调用另一个同步方法,这会增加死锁风险。
10. 面试常见问题
10.1 如何避免死锁?
可以从破坏死锁的四个必要条件角度回答:
- 破坏互斥条件(不总是可行)
- 破坏占有且等待(一次性申请所有资源)
- 破坏非抢占条件(设置锁超时)
- 破坏循环等待(统一锁获取顺序)
10.2 死锁和活锁的区别?
死锁是线程互相等待对方释放资源,所有线程都被阻塞;活锁是线程不断重试某个操作但始终无法成功,线程并未阻塞但无法继续执行。
10.3 如何设计一个无锁的并发系统?
可以考虑使用:
- 原子变量(AtomicInteger等)
- CAS操作
- 不可变对象
- 线程本地存储
- 消息队列等异步机制