大家好,我是锋哥。今天分享关于【高频面试题:Java死锁问题如何解决?】面试题。希望对大家有帮助;
高频面试题:Java死锁问题如何解决?
死锁产生的四个必要条件(缺一不可)
- 互斥(Mutual Exclusion)—— 锁只能被一个线程持有
- 持有并等待(Hold and Wait)—— 持有至少一个锁,同时等待另一个锁
- 不可抢占(No Preemption)—— 锁只能由持有者主动释放
- 循环等待(Circular Wait)—— 线程之间形成环形等待链
解决死锁的核心思想:破坏上面任意一个条件(最常用的是破坏第4个,其次是第2个)。
实际开发中最有效的解决 & 预防手段(按推荐优先级排序)
| 优先级 | 方法 | 核心思想 | 适用场景 | 缺点/代价 | 代码示例关键点 |
|---|---|---|---|---|---|
| ★★★★★ | 固定锁获取顺序(Lock Ordering) | 破坏循环等待 | 绝大多数业务场景 | 需要提前规划锁的全局顺序 | 所有线程都按相同顺序获取锁(如按id排序) |
| ★★★★☆ | 使用带超时的锁(tryLock) | 破坏不可抢占 + 持有并等待 | 锁竞争激烈、容易死锁的场景 | 业务需处理获取失败的情况 | ReentrantLock.tryLock(时间, 时间单位) |
| ★★★★☆ | 减小锁粒度 + 缩短锁持有时间 | 减少持有并等待时间窗口 | 锁保护范围过大的代码 | 代码改动较大 | 只在真正需要线程安全的地方加锁 |
| ★★★☆☆ | 避免嵌套锁 / 锁的交叉使用 | 避免持有并等待 | 老代码重构、复杂业务逻辑 | 有时难以完全消除 | 尽量把获取第二个锁的代码移出第一个锁范围 |
| ★★★☆☆ | 一次性申请所有资源 | 破坏持有并等待 | 资源有限且数量可知的场景 | 实现复杂,容易浪费资源 | 类似银行家算法或先尝试获取所有锁 |
| ★★☆☆☆ | 使用更高级的并发工具 | 从根本上减少显式加锁 | 可以用新结构替换旧代码的地方 | 学习成本、改造成本较高 | ConcurrentHashMap、CopyOnWrite、StampedLock、CompletableFuture 等 |
| ★★☆☆☆ | 死锁检测 + 自动恢复 | 事后补救(非首选) | 极难避免死锁的遗留系统 | 业务可能受损,复杂 | ThreadMXBean.findDeadlockedThreads() |
最常用、最推荐的代码实践写法对比
容易死锁的写法(经典转账示例)
public void transfer(Account from, Account to, int amount) { synchronized (from) { synchronized (to) { // 嵌套锁 + 顺序不一致 → 极易死锁 // 转账逻辑 } } }推荐写法1:固定锁顺序(最常用、最安全)
public void transfer(Account from, Account to, int amount) { // 按账户ID的自然顺序加锁(破坏循环等待) Account first = from.getId() < to.getId() ? from : to; Account second = first == from ? to : from; synchronized (first) { synchronized (second) { // 转账逻辑 } } }推荐写法2:使用带超时的 ReentrantLock(更灵活)
private final ReentrantLock lock1 = new ReentrantLock(); private final ReentrantLock lock2 = new ReentrantLock(); public boolean transferWithTimeout() { try { if (!lock1.tryLock(5, TimeUnit.SECONDS)) { return false; // 或抛出自定义异常 } try { if (!lock2.tryLock(5, TimeUnit.SECONDS)) { return false; } try { // 转账逻辑 return true; } finally { lock2.unlock(); } } finally { lock1.unlock(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); return false; } }快速记忆口诀(生产环境最常说的一句话)
“要么锁的顺序全局统一,要么加锁带超时,其他都是辅助手段。”
最后几条生产环境铁律
- 尽量避免在 synchronized 块里调用外部方法(尤其是可能阻塞、IO、网络、加锁的方法)
- 锁保护的范围越小越好,能用局部变量锁就别用对象锁
- 能用
java.util.concurrent包下的工具就尽量别手写 synchronized + wait/notify - 定期做代码审查,特别关注多个锁同时出现的业务逻辑