Java并发编程中的死锁问题与预防

死锁典型模式是多线程以不同顺序获取同一组对象锁,如线程1先锁objA再锁objB、线程2反之,导致互相等待;jstack可直接定位,输出“Found one Java-level deadlock:”;避免关键是固定锁顺序、用tryLock+回退、优先并发工具类。

死锁发生的典型代码模式

Java中死锁最常见于多个线程以不同顺序获取同一组Object锁(或synchronized块),且都持有部分锁、等待对方释放。比如两个线程分别执行:

Thread 1: synchronized(objA) { ... synchronized(objB) { ... } }
Thread 2: synchronized(objB) { ... synchronized(objA) { ... } }

一旦线程1拿到objA、线程2拿到objB,双方就会永久阻塞——JVM不会自动检测或中断这种等待。

如何用jstack定位死锁

运行中的Java进程若疑似卡死,优先用jstack抓取线程快照,它能直接标出死锁线程及锁依赖链:

  • 执行 jstack ,输出中搜索 Found one Java-level deadlock:
  • 若没找到,加参数 -ljstack -l )可显示更详细的锁信息,包括ReentrantLock持有者
  • 注意:jstack必须由与目标JVM相同用户执行,否则可能无权限读取

避免嵌套锁的实用策略

根本解法不是“加超时”或“重试”,而是从设计上消除锁序不一致:

  • 所有线程按**固定顺序**获取锁,例如约定按System.identityHashCode(objA) 决定先锁哪个对象
  • tryLock(long, TimeUnit)替代synchronized,失败后释放已持锁并重试(需配合lockInterruptibly()处理中断)
  • 优先使用java.util.concurrent工具类(如ConcurrentHashMapAtomicInteger),它们内部不依赖用户显式锁,天然规避死锁风险

ReentrantLock与synchronized的死锁差异

synchronized是JVM层实现,不可中断、无超时;而Ree

ntrantLock支持更精细控制,但误用反而更容易引发隐蔽死锁:

  • 忘记在finally块中调用unlock() → 锁永远不释放 → 其他线程卡死
  • 同一个ReentrantLock实例被不同线程反复lock()但未配对unlock() → 实际等效于无限嵌套
  • lockInterruptibly()虽可响应中断,但如果线程在等待锁时被中断,需确保清理资源,否则状态可能不一致

真正难防的不是“两个锁互相等”,而是锁粒度混乱 + 异常路径遗漏 + 多层调用隐藏了锁获取逻辑——这些地方最容易漏掉unlock()或搞错锁顺序。