Java多线程死锁问题排查与预防实战指南

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.txt

4.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 -b

8. 数据库死锁排查

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 如何避免死锁?

可以从破坏死锁的四个必要条件角度回答:

  1. 破坏互斥条件(不总是可行)
  2. 破坏占有且等待(一次性申请所有资源)
  3. 破坏非抢占条件(设置锁超时)
  4. 破坏循环等待(统一锁获取顺序)

10.2 死锁和活锁的区别?

死锁是线程互相等待对方释放资源,所有线程都被阻塞;活锁是线程不断重试某个操作但始终无法成功,线程并未阻塞但无法继续执行。

10.3 如何设计一个无锁的并发系统?

可以考虑使用:

  • 原子变量(AtomicInteger等)
  • CAS操作
  • 不可变对象
  • 线程本地存储
  • 消息队列等异步机制