Skip to content

Java 中什么情况会导致死锁?如何避免? ​

  • 标签:Java并发、Java

🏛 业务架构师 ​

死锁的四个必要条件:互斥、持有并等待、不可剥夺、循环等待。业务上最容易制造死锁的是两个锁的加锁顺序不一致:

经典场景——转账:事务 A(转账:先锁账户 1 再锁账户 2)与事务 B(退款:先锁 2 再锁 1)并发,互相持有对方要的锁,永久等待。数据库里的批量更新顺序不一致(A 按 ID 升序、B 按降序)同理。

我的预防与治理体系:

事前(设计期,成本最低)

  • 统一加锁顺序协议:所有业务路径按同一规则排序(账户 ID 升序、订单号排序)再逐个加锁——这是业务协议,要写进设计文档与代码规约,靠约定消灭循环等待;
  • 锁的持有时间最小化:锁内禁止远程调用、大计算、IO;事务里的 RPC 是头号杀手;
  • 一次拿全所需锁,避免"拿着 A 去申请 B"的渐进式获取。

事中(运行期)

  • 数据库靠 InnoDB 死锁检测自动回滚一方,业务捕获死锁异常并有限重试(指数退避)——这是必须写的韧性代码;
  • JVM 锁用 tryLock(timeout) 替代无限 lock,拿不到就放弃并释放已持有的锁,破坏"持有并等待";
  • Java 排查:jstack 看 "Found one Java-level deadlock",数据库看 SHOW ENGINE INNODB STATUS 的 LATEST DETECTED DEADLOCK。

事后(治理)

  • 死锁率监控告警——突增通常对应某次业务变更(新查询路径、批任务改时段),把死锁当业务信号而不只是技术事件。

一句话:死锁的解药一半在协议(顺序),一半在韧性(超时+重试),两者都要制度化。

🏗 系统架构师 ​

死锁产生的四个必要条件:互斥、持有并等待、不可剥夺、循环等待——四个同时成立才死锁,工程上打破任意一个即可。

Java 里典型场景:嵌套获取多把锁顺序不一致(A 线程先锁 X 再锁 Y,B 线程先 Y 再 X);线程池场景的相互等待任务(父任务提交子任务并 get(),子任务排在队列里永远轮不到执行);两把数据库行锁交叉更新。

我的预防与治理:

  • 统一加锁顺序(打破循环等待,最常用):所有代码路径按全局顺序拿锁,批量操作先排序再加锁;
  • tryLock 带超时(打破不可剥夺):lock.tryLock(1, TimeUnit.SECONDS) 失败释放已持有的锁退避重试,死锁退化为重试;
  • 缩小锁范围、锁内不做耗时操作(RPC、IO),减少交叉窗口;
  • 线程池隔离 + 拒绝策略,杜绝任务互相等待;Java 21 虚拟线程下同步块内再阻塞的 pinning 问题也要注意;
  • 开放调用(alien method 移出锁)、用不可变对象减少共享。

排查工具:jstack 直接标注 "Found one Java-level deadlock";ThreadMXBean.findDeadlockedThreads() 可做运行时检测自动告警。数据库侧则靠 InnoDB 死锁检测自动回滚 + 应用重试。原则:用超时把死锁从故障降级为可重试错误,是架构韧性的一部分。

💻 全栈开发专家 ​

死锁四条件:互斥、持有并等待、不可剥夺、循环等待——同时成立才死锁。代码层面就是两个线程互相持有对方需要的锁且都不放手。

java
// 经典互相加锁
Thread1: lock(A) → 请求 lock(B)
Thread2: lock(B) → 请求 lock(A)

避免手段(按工程频率):

  1. 统一加锁顺序:所有线程都按 A→B 顺序拿锁,循环等待被打破。批量操作先按主键排序再逐条锁;
  2. tryLock 带超时:拿不到就放弃已持有的锁并重试,破坏"持有并等待":
java
if (lockA.tryLock(1, TimeUnit.SECONDS)) {
    try {
        if (lockB.tryLock(1, TimeUnit.SECONDS)) { try { doWork(); } finally { lockB.unlock(); } }
    } finally { lockA.unlock(); }
}
  1. 缩小锁范围、锁内不做 IO/RPC/sleep;
  2. 减少锁粒度:分段锁、ConcurrentHashMap、LongAdder 代替同步计数;
  3. 用无锁结构(CAS、阻塞队列)重构。

排查(实操命令):

bash
jstack <pid> | grep -A 20 "Found one Java-level deadlock"  # 直接给出死锁对
# 或用 arthas:thread -b 一键找阻塞源头

线上预防:压测覆盖并发路径;对外部调用(DB 行锁死锁)catch 死锁异常(SQLState 40001)做自动重试;监控线程 BLOCKED 数量突增。