1. 什么是死锁?
死锁(Deadlock) 是指两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象,导致所有线程都无法继续执行下去。
死锁的四个必要条件(Coffman条件)
-
互斥条件:资源一次只能被一个线程占用。
-
占有并等待:线程持有至少一个资源,并等待获取其他资源。
-
不可抢占:已分配给线程的资源不能被强行剥夺,必须由线程自行释放。
-
循环等待:多个线程形成头尾相接的等待资源关系。
📌 注意:必须同时满足这四个条件才会发生死锁,破坏任意一个即可避免死锁。
2. 典型的死锁场景
(1) 互斥锁嵌套
public class DeadlockExample {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread1 holds lock1");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock2) {
System.out.println("Thread1 holds lock2");
}
}
}).start();
new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread2 holds lock2");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock1) {
System.out.println("Thread2 holds lock1");
}
}
}).start();
}
}
🔍 分析:
-
Thread1持有lock1,等待lock2。 -
Thread2持有lock2,等待lock1。 -
循环等待导致死锁。
(2) 数据库事务死锁
-- 事务1
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
-- 事务2
BEGIN;
UPDATE accounts SET balance = balance - 50 WHERE id = 2;
UPDATE accounts SET balance = balance + 50 WHERE id = 1;
COMMIT;
🔍 分析:
-
事务1 锁定
id=1,等待id=2。 -
事务2 锁定
id=2,等待id=1。 -
数据库检测到死锁后会自动回滚其中一个事务。
3. 死锁的解决方案
(1) 破坏"占有并等待"
-
一次性申请所有资源(避免部分持有):
synchronized (lock1) { synchronized (lock2) { // 业务逻辑 } }✅ 优点:简单直接。
❌ 缺点:可能导致资源浪费(长时间占用锁)。
(2) 破坏"不可抢占"
-
使用
tryLock超时机制(ReentrantLock):if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) { try { if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) { try { // 业务逻辑 } finally { lock2.unlock(); } } } finally { lock1.unlock(); } }✅ 优点:避免无限等待。
❌ 缺点:代码复杂度高。
(3) 破坏"循环等待"
-
按固定顺序获取锁(避免交叉依赖):
// 定义全局锁获取顺序 private static final Object[] locks = {lock1, lock2}; public void doWork() { synchronized (locks[0]) { synchronized (locks[1]) { // 业务逻辑 } } }✅ 优点:彻底避免循环等待。
❌ 缺点:需要统一锁管理策略。
(4) 死锁检测与恢复
-
JVM 检测死锁(
jstack或ThreadMXBean):ThreadMXBean bean = ManagementFactory.getThreadMXBean(); long[] threadIds = bean.findDeadlockedThreads(); if (threadIds != null) { ThreadInfo[] infos = bean.getThreadInfo(threadIds); for (ThreadInfo info : infos) { System.out.println("Deadlocked Thread: " + info.getThreadName()); } }✅ 适用场景:调试环境。
❌ 生产环境:通常直接重启服务。
4. 如何预防死锁?
| 策略 | 实现方式 | 适用场景 |
|---|---|---|
| 锁顺序固定 | 全局约定锁的获取顺序 | 多锁竞争场景(如转账) |
| 超时释放 | tryLock + 超时机制 | 高并发系统 |
| 减小锁粒度 | 使用更细粒度的锁(如分段锁) | 缓存、哈希表等数据结构 |
| 无锁编程 | CAS(如 AtomicInteger) | 计数器、状态标志 |
| 避免嵌套锁 | 重构代码减少锁嵌套 | 复杂业务逻辑 |
5. 回答技巧
面试官:"如何避免死锁?"
推荐回答:
"死锁的四个必要条件是互斥、占有等待、不可抢占和循环等待。我们可以通过以下方式避免:
固定锁顺序:确保所有线程按相同顺序获取锁(如
lock1 → lock2)。超时机制:使用
tryLock设置超时,避免无限等待。减少锁粒度:用更细粒度的锁(如
ConcurrentHashMap的分段锁)。无锁数据结构:如
AtomicInteger或CAS操作。
实际项目中,我会优先评估锁的必要性,尽量用无锁方案,必要时采用锁顺序+超时策略。"
6. 总结
-
死锁必须满足四个条件,破坏任意一个即可避免。
-
解决方案:锁顺序固定、超时机制、死锁检测。
-
预防比修复更重要:代码审查时注意锁的嵌套和顺序。
通过理解死锁原理并采用合理策略,可以显著降低多线程程序的死锁风险! 🚀
8021

被折叠的 条评论
为什么被折叠?



