死锁是资源调度逻辑的必然结果,只要存在多个线程、多个锁且加锁顺序不一致,就必然发生;唯一可稳定破坏的条件是循环等待,须通过统一锁序(如按ID从小到大)、工具类封装加锁逻辑、覆盖异常分支等手段根治。

死锁不是偶发 bug,而是资源调度逻辑的必然结果
只要代码里存在「多个线程 + 多个锁 + 加锁顺序不一致」这三种要素,死锁就不是“会不会发生”的问题,而是“什么时候发生”的问题。它不会总在测试环境复现,但上线后可能突然卡住某个关键路径——比如支付回调线程和库存更新线程互相等待对方持有的锁,整个订单流程就挂住了。
循环等待是唯一可稳定破坏的死锁条件
互斥、持有并等待、不可剥夺这三个条件,要么是锁机制本身决定的(如 synchronized 和 ReentrantLock 天然互斥且不可抢占),要么在业务逻辑中难以根除(比如一个事务必须先锁订单再锁用户,无法合并为单次申请)。唯独「循环等待」完全由程序员控制:它只取决于你让线程按什么顺序去拿锁。
- 错误写法:
t1先lockA.lock()再lockB.lock();t2反过来先lockB.lock()再lockA.lock() - 正确做法:所有线程统一按
lockA→lockB顺序获取,哪怕当前只用到lockB,也要先确保lockA已释放或未被争抢 - 资源编号建议:给每个锁对象加唯一整型 ID(如
lockA.id = 101,lockB.id = 102),加锁前比较 ID,只允许从小到大申请
实际编码中怎么落地锁排序
不能靠文档约定或口头提醒,必须把顺序检查变成代码逻辑的一部分。否则重构时随手调换两行 lock(),就埋下隐患。
- 用工具类封装:写一个
LockOrdering工具,接收多个Lock实例,按 ID 排序后依次lock(),失败则全部回滚 - 避免在不同方法分散加锁:像
updateOrder()和deductStock()各自持有一把锁,就很难统一顺序;应合并为processOrderAndStock(),在单一入口处完成全部锁申请 - 注意锁粒度陷阱:用
ReentrantLock代替synchronized不代表更安全;如果synchronized(this)和lockA混用,它们之间也构成潜在循环等待链
超时和 tryLock 是补救手段,不是预防方案
lock.tryLock(3, TimeUnit.SECONDS) 能让线程不无限等待,但它只是把死锁变成「偶发性失败」,而不是消除死锁成因。日志里看到一堆 InterruptedException 或 TimeoutException,往往说明循环等待已经存在,只是被超时截断了。
- 超时值设太短:业务正常耗时波动就可能触发,误报率高
- 超时值设太长:等同于没设,还是卡住几秒甚至几分钟
- 真正该做的:在压测阶段用
jstack抓取线程栈,搜索waiting for lock和locked配对,直接定位哪两个锁在交叉等待
最易被忽略的一点:锁排序必须覆盖所有路径,包括异常分支。比如 try 块里按 A→B 加锁,catch 中却只释放了 B,A 泄漏了——下次再进这个流程,A 就成了“幽灵锁”,其他线程永远等不到它。










