死锁不是异常,jvm不会自动检测或抛出deadlockexception;需通过threadmxbean主动检测,且检测后须人工干预(如中断、降级或重启),而非依赖自恢复。

死锁异常(DeadlockDetection)不会自动抛出,JVM 不会主动“检测并抛出”死锁
Java 本身没有 DeadlockException 这类运行时异常。所谓“死锁异常”,其实是你通过 ThreadMXBean.findDeadlockedThreads() 主动检测到线程陷入循环等待后,自己决定怎么处理——JVM 不会中断线程、不会抛异常、更不会自动恢复。
常见错误现象:
• 应用卡住,CPU 低,线程堆栈里一堆 WAITING (on object monitor)
• 用 jstack -l 看到 “Found 1 deadlock.”,但程序没崩溃也没报错
• 误以为加了 synchronized 就会触发某种“死锁保护机制”
- 死锁是状态,不是事件:它不触发回调,也不进入异常处理流程
- 检测必须主动轮询或在监控入口(如健康检查端点)中调用
ThreadMXBean - 生产环境慎用高频轮询:获取线程信息有同步开销,频繁调用可能拖慢 JVM
用 ThreadMXBean 检测死锁后,不能只打日志,得做隔离或降级
检测到死锁只是第一步。如果只是记录日志然后继续运行,大概率后续请求持续失败——因为死锁线程组仍卡在 monitor 上,资源未释放,新请求进来大概率复现相同阻塞。
使用场景:
• 微服务健康检查接口中嵌入死锁探测
• 批处理任务启动前做一次线程快照校验
立即学习“Java免费学习笔记(深入)”;
- 不要仅依赖
findDeadlockedThreads()返回非 null 就认为“已解决”:它只返回线程 ID 数组,不提供锁持有链细节 - 配合
ThreadMXBean.getThreadInfo(ids, true, true)获取带锁信息的完整堆栈,才能定位哪两个对象被交叉加锁 - 自恢复 ≠ 自动解锁:Java 不允许从外部强制释放 synchronized 锁或
ReentrantLock,唯一可行路径是让持有锁的线程自然退出(比如超时中断 + 可中断锁)
用 ReentrantLock 替代 synchronized 是为获得可中断性,不是为“避免死锁”
很多人换用 ReentrantLock 是冲着“能 tryLock + timeout”去的,这确实能降低死锁概率,但不能消除逻辑层面的循环等待。关键区别在于:当发生阻塞时,你能用 lockInterruptibly() 响应中断,从而有机会清理状态、释放已占资源、退出线程。
参数差异:
• synchronized:无超时,不可中断,一旦进入等待就只能等锁释放
• ReentrantLock.lockInterruptibly():可被 Thread.interrupt() 中断,抛 InterruptedException,让你有机会 catch 并释放前置资源
- 必须配合“锁顺序约定”:多个锁按固定顺序 acquire,否则
tryLock也救不了逻辑错误 - 别忘了在
finally块里unlock(),否则比synchronized更容易泄漏锁 - 性能影响很小,但可读性下降:显式锁要求开发者对生命周期完全负责
系统级自恢复的边界很窄,别指望 JVM 或 Spring 帮你“重启线程”
所谓“自恢复”,在 Java 进程内实际只有三条路:中断卡住线程、重启整个 JVM、或把问题请求路由到备用实例。没有中间态。
容易踩的坑:
• 试图用 Thread.stop() 强制终止死锁线程 → 已废弃,且会导致锁未释放、对象状态不一致
• 在 Spring @Transactional 方法里捕获“疑似死锁”后手动 rollback → 数据库死锁由 DBMS 报 SQLTimeoutException 或 PessimisticLockException,和 JVM 线程死锁无关,别混为一谈
• 给所有 service 方法加统一超时注解(如 @Timeable),但没配线程中断机制 → 超时只是 Future.cancel(),若底层线程没响应中断,照样卡死
- 最现实的自恢复动作:记录死锁现场 → 触发告警 → 通知运维重启该实例(K8s 下就是滚动重启 pod)
- 如果必须保进程,唯一可控出口是让业务线程自己定期检查中断状态(
Thread.currentThread().isInterrupted())并在安全点退出 - 真正的难点不在检测,而在判断“哪些线程可以安全中断”——比如 IO 正在写磁盘、事务正提交一半,中断就等于数据损坏










