外观
死锁是什么?
⭐ 题目日期:
京东 - 2024/12/26
📝 题解:
死锁(Deadlock) 是多线程编程中一种资源竞争引发的僵局,指两个或多个线程在运行过程中因争夺资源而陷入无限等待的状态,导致程序无法继续执行。死锁是并发编程中需要重点防范的严重问题。
死锁产生的必要条件
要形成死锁,必须同时满足以下四个条件:
互斥条件(Mutual Exclusion)
- 资源一次只能被一个线程占用(如锁、文件句柄等)。
持有并等待(Hold and Wait)
- 线程已持有至少一个资源,同时又在等待其他线程持有的资源。
不可抢占(No Preemption)
- 资源只能由持有它的线程主动释放,不能被其他线程强制抢占。
循环等待(Circular Wait)
- 存在一组线程,每个线程都在等待下一个线程所持有的资源,形成环形依赖链。
经典死锁示例(Java代码)
public class DeadlockExample {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lockA) {
System.out.println("Thread1 持有 lockA");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lockB) { // 等待 lockB
System.out.println("Thread1 获得 lockB");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lockB) {
System.out.println("Thread2 持有 lockB");
synchronized (lockA) { // 等待 lockA
System.out.println("Thread2 获得 lockA");
}
}
});
thread1.start();
thread2.start();
}
}
输出:
Thread1 持有 lockA
Thread2 持有 lockB
(程序卡住,无法继续执行)
如何避免死锁
破坏死锁的四个必要条件中的至少一个即可避免死锁:
1. 破坏循环等待
- 统一资源申请顺序:所有线程按固定顺序获取资源。
修改上述示例,让thread1
和thread2
按相同顺序获取锁:// thread2 修改为: synchronized (lockA) { // 先获取 lockA System.out.println("Thread2 持有 lockA"); synchronized (lockB) { System.out.println("Thread2 获得 lockB"); } }
2. 破坏持有并等待
- 原子性获取所有资源:一次性申请全部所需资源,若无法获取则释放已持有的资源。
示例:使用ReentrantLock
的tryLock()
方法:Lock lockA = new ReentrantLock(); Lock lockB = new ReentrantLock(); public void doWork() { while (true) { if (lockA.tryLock()) { try { if (lockB.tryLock()) { try { // 执行任务 return; } finally { lockB.unlock(); } } } finally { lockA.unlock(); } } // 短暂等待后重试 try { Thread.sleep(100); } catch (InterruptedException e) {} } }
3. 允许资源抢占
- 设置超时机制:尝试获取资源时设定超时时间,超时后释放已有资源并重试。
示例:使用Lock.tryLock(long time, TimeUnit unit)
:if (lockA.tryLock(500, TimeUnit.MILLISECONDS)) { try { if (lockB.tryLock(500, TimeUnit.MILLISECONDS)) { try { // 执行任务 } finally { lockB.unlock(); } } } finally { lockA.unlock(); } }
4. 减少互斥(非万能)
- 使用无锁数据结构(如
ConcurrentHashMap
)或原子变量(如AtomicInteger
),但仅适用于特定场景。
检测与恢复
死锁检测
- 使用工具(如
jstack
、VisualVM
)分析线程转储(Thread Dump),查找阻塞的线程和持有的锁。 - 代码示例:通过
ThreadMXBean
检测死锁:ThreadMXBean bean = ManagementFactory.getThreadMXBean(); long[] threadIds = bean.findDeadlockedThreads(); if (threadIds != null) { System.out.println("检测到死锁!"); }
- 使用工具(如
强制恢复
- 重启应用或终止部分线程(极端情况,可能导致数据不一致)。
实际开发中的建议
- 避免嵌套锁:尽量减少锁的嵌套使用。
- 缩小锁范围:只在必要代码块加锁,尽快释放锁。
- 使用高级工具:优先选择
java.util.concurrent
包中的并发容器(如BlockingQueue
)和同步器(如CountDownLatch
)。 - 代码审查:在团队协作中,重点检查多线程代码的锁顺序和资源依赖。
总结
死锁是并发编程中的“隐形杀手”,需通过合理设计资源管理策略、统一加锁顺序、使用超时机制等方式预防。理解其成因并掌握排查工具,是构建高可靠性多线程应用的必备技能。