死锁的产生与避免
-
- 一、死锁的产生原因
- 二、典型死锁场景示例
- 三、死锁的检测与诊断
-
- [1. 使用工具检测死锁](#1. 使用工具检测死锁)
- [2. 日志分析](#2. 日志分析)
- 四、死锁的避免策略
-
- [1. 破坏"请求与保持"条件](#1. 破坏“请求与保持”条件)
- [2. 破坏"不可剥夺"条件](#2. 破坏“不可剥夺”条件)
- [3. 破坏"循环等待"条件](#3. 破坏“循环等待”条件)
- [4. 使用超时机制](#4. 使用超时机制)
- [5. 减少锁的粒度](#5. 减少锁的粒度)
- 五、最佳实践总结
- 六、总结
一、死锁的产生原因
死锁是多个线程(或进程)因竞争资源而陷入无限等待的状态,需同时满足以下 四个必要条件:
-
互斥条件(Mutual Exclusion)
- 资源一次只能被一个线程独占使用(如锁、文件句柄)。
-
请求与保持(Hold and Wait)
- 线程在持有至少一个资源的同时,请求其他线程占有的资源。
-
不可剥夺(No Preemption)
- 资源只能由持有者主动释放,不能被强制抢占。
-
循环等待(Circular Wait)
- 多个线程形成环形等待链,每个线程都在等待下一个线程释放资源。
二、典型死锁场景示例
Java 代码示例:
java
public class DeadlockDemo {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (lockA) {
System.out.println("Thread1 holds lockA");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lockB) { // 等待Thread2释放lockB
System.out.println("Thread1 holds lockB");
}
}
}).start();
new Thread(() -> {
synchronized (lockB) {
System.out.println("Thread2 holds lockB");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lockA) { // 等待Thread1释放lockA
System.out.println("Thread2 holds lockA");
}
}
}).start();
}
}
结果 :
两个线程互相等待对方释放锁,程序无限卡死。
三、死锁的检测与诊断
1. 使用工具检测死锁
-
jstack (Java自带工具):
bashjstack <pid> # 输出线程快照,显示死锁的线程及持有/等待的锁
-
VisualVM 或 JConsole :
图形化界面查看线程状态,直接标记死锁。
2. 日志分析
若日志中线程长时间处于 BLOCKED
状态且无进展,可能发生死锁。
四、死锁的避免策略
1. 破坏"请求与保持"条件
- 一次性申请所有资源 :
线程在开始执行前申请全部所需资源,否则不执行。
缺点:资源利用率低,可能导致饥饿。
2. 破坏"不可剥夺"条件
- 允许抢占资源 :
若线程请求资源失败,强制释放已持有的资源(需支持回滚操作)。
缺点:实现复杂,适用于特定场景(如数据库事务)。
3. 破坏"循环等待"条件
-
资源有序分配法 :
为所有资源类型定义全局顺序,线程按顺序申请资源。
示例 :规定必须先申请
lockA
再申请lockB
,避免交叉申请。java// 修改后代码:两个线程均按 lockA → lockB 顺序申请 new Thread(() -> { synchronized (lockA) { synchronized (lockB) { /* 逻辑 */ } } }).start(); new Thread(() -> { synchronized (lockA) { synchronized (lockB) { /* 逻辑 */ } } }).start();
4. 使用超时机制
-
尝试获取锁时设置超时 :
若在指定时间内未获得锁,放弃并释放已持有的资源,避免无限等待。
Java实现 (使用ReentrantLock
):javaLock lockA = new ReentrantLock(); Lock lockB = new ReentrantLock(); if (lockA.tryLock(1, TimeUnit.SECONDS)) { try { if (lockB.tryLock(1, TimeUnit.SECONDS)) { try { /* 逻辑 */ } finally { lockB.unlock(); } } } finally { lockA.unlock(); } }
5. 减少锁的粒度
- 缩小同步范围 :
仅对必要代码加锁,减少锁的持有时间。 - 使用线程安全的数据结构 :
如ConcurrentHashMap
替代synchronized
+HashMap
。
五、最佳实践总结
策略 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
资源有序分配 | 多锁交叉申请场景 | 简单有效,预防循环等待 | 需全局统一顺序,可能限制灵活性 |
超时机制 | 高并发、允许重试的场景 | 避免无限等待,提升系统健壮性 | 需处理超时重试逻辑 |
无锁编程(CAS、原子类) | 低竞争、简单操作场景 | 高性能,无死锁风险 | 复杂逻辑实现困难 |
事务回滚 | 数据库、支持回滚的操作 | 保证数据一致性 | 实现成本高 |
六、总结
死锁的避免需结合业务场景选择合适的策略:
- 关键系统(如金融交易):优先使用资源有序分配和超时机制。
- 高并发系统:减少锁粒度,采用无锁数据结构。
- 复杂事务:结合事务管理和回滚机制。
通过代码规范、工具检测和设计优化,可显著降低死锁发生概率。