云计算百科
云计算领域专业知识百科平台

死锁问题面试全攻略

💀 死锁问题面试全攻略 – 从原理到实战的完整指南

🎯 前言:为什么死锁是面试必考题?

在所有操作系统概念中,死锁可能是最容易理解但也最容易出错的概念。几乎每个技术面试都会涉及死锁问题,因为它不仅考查你对并发编程的理解,更能反映你解决复杂问题的思维能力。

掌握死锁,你将能够:

  • ✅ 写出更安全的多线程代码
  • ✅ 快速诊断和解决系统卡死问题
  • ✅ 在面试中展现扎实的计算机基础
  • ✅ 设计出更健壮的分布式系统

🔍 什么是死锁?

经典定义

死锁(Deadlock):两个或多个进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉,它们都将无法推进下去。

生活中的死锁例子

在这里插入图片描述

场景描述:

  • 小明手里拿着房间A的钥匙,但需要房间B的钥匙才能完成任务
  • 小红手里拿着房间B的钥匙,但需要房间A的钥匙才能完成任务
  • 两人都不愿意先放手,结果谁都无法前进

这就是典型的死锁场景!

📋 死锁产生的四个必要条件

死锁的发生需要同时满足以下四个条件,缺一不可:

在这里插入图片描述

详细解析每个条件

1. 🔒 互斥条件(Mutual Exclusion)

含义:资源在同一时间只能被一个进程使用

实际例子:

✅ 互斥资源:
– 打印机:不能同时打印两个文档
– 文件写锁:不能同时写入同一文件
– 数据库行锁:不能同时修改同一行数据
– 网络端口:同一端口不能被多个程序绑定

❌ 非互斥资源:
– CPU时间:可以通过时间片共享
– 内存:可以同时被多个进程使用不同区域
– 只读文件:可以被多个进程同时读取

2. 🤲 占有和等待条件(Hold and Wait)

含义:进程至少占有一个资源,同时又在等待获取其他资源

代码示例场景:

// 线程1
synchronized(lockA) { // 占有资源A
// 做一些工作…
synchronized(lockB) { // 等待资源B
// 使用A和B
}
}

// 线程2
synchronized(lockB) { // 占有资源B
// 做一些工作…
synchronized(lockA) { // 等待资源A
// 使用B和A
}
}

3. 🚫 不可剥夺条件(No Preemption)

含义:资源不能被强制性地从进程中剥夺,只能由占有它的进程主动释放

对比分析:

可剥夺资源:
✅ CPU时间片:操作系统可以强制切换
✅ 内存页:可以被交换到磁盘
✅ 网络带宽:可以被重新分配

不可剥夺资源:
❌ 文件锁:必须等待持有者释放
❌ 信号量:不能强制释放
❌ 互斥锁:必须由获得者释放
❌ 数据库连接:不能强制断开正在使用的连接

4. 🔄 循环等待条件(Circular Wait)

含义:存在一个进程资源的循环等待链

等待链示例:

#mermaid-svg-w9Md870lKBTLQbGW {font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-w9Md870lKBTLQbGW .error-icon{fill:#552222;}#mermaid-svg-w9Md870lKBTLQbGW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-w9Md870lKBTLQbGW .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-w9Md870lKBTLQbGW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-w9Md870lKBTLQbGW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-w9Md870lKBTLQbGW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-w9Md870lKBTLQbGW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-w9Md870lKBTLQbGW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-w9Md870lKBTLQbGW .marker.cross{stroke:#333333;}#mermaid-svg-w9Md870lKBTLQbGW svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-w9Md870lKBTLQbGW .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-w9Md870lKBTLQbGW .cluster-label text{fill:#333;}#mermaid-svg-w9Md870lKBTLQbGW .cluster-label span{color:#333;}#mermaid-svg-w9Md870lKBTLQbGW .label text,#mermaid-svg-w9Md870lKBTLQbGW span{fill:#333;color:#333;}#mermaid-svg-w9Md870lKBTLQbGW .node rect,#mermaid-svg-w9Md870lKBTLQbGW .node circle,#mermaid-svg-w9Md870lKBTLQbGW .node ellipse,#mermaid-svg-w9Md870lKBTLQbGW .node polygon,#mermaid-svg-w9Md870lKBTLQbGW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-w9Md870lKBTLQbGW .node .label{text-align:center;}#mermaid-svg-w9Md870lKBTLQbGW .node.clickable{cursor:pointer;}#mermaid-svg-w9Md870lKBTLQbGW .arrowheadPath{fill:#333333;}#mermaid-svg-w9Md870lKBTLQbGW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-w9Md870lKBTLQbGW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-w9Md870lKBTLQbGW .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-w9Md870lKBTLQbGW .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-w9Md870lKBTLQbGW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-w9Md870lKBTLQbGW .cluster text{fill:#333;}#mermaid-svg-w9Md870lKBTLQbGW .cluster span{color:#333;}#mermaid-svg-w9Md870lKBTLQbGW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-w9Md870lKBTLQbGW :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}进程P1等待资源R2进程P2等待资源R3进程P3等待资源R1资源R1被P1占有资源R2被P2占有资源R3被P3占有

循环等待链:P1 → R2 → P2 → R3 → P3 → R1 → P1

🎭 经典死锁案例分析

案例1:哲学家就餐问题

#mermaid-svg-XYws95RgjRhaOM4s {font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-XYws95RgjRhaOM4s .error-icon{fill:#552222;}#mermaid-svg-XYws95RgjRhaOM4s .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XYws95RgjRhaOM4s .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-XYws95RgjRhaOM4s .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XYws95RgjRhaOM4s .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XYws95RgjRhaOM4s .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XYws95RgjRhaOM4s .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XYws95RgjRhaOM4s .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XYws95RgjRhaOM4s .marker.cross{stroke:#333333;}#mermaid-svg-XYws95RgjRhaOM4s svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XYws95RgjRhaOM4s .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-XYws95RgjRhaOM4s .cluster-label text{fill:#333;}#mermaid-svg-XYws95RgjRhaOM4s .cluster-label span{color:#333;}#mermaid-svg-XYws95RgjRhaOM4s .label text,#mermaid-svg-XYws95RgjRhaOM4s span{fill:#333;color:#333;}#mermaid-svg-XYws95RgjRhaOM4s .node rect,#mermaid-svg-XYws95RgjRhaOM4s .node circle,#mermaid-svg-XYws95RgjRhaOM4s .node ellipse,#mermaid-svg-XYws95RgjRhaOM4s .node polygon,#mermaid-svg-XYws95RgjRhaOM4s .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-XYws95RgjRhaOM4s .node .label{text-align:center;}#mermaid-svg-XYws95RgjRhaOM4s .node.clickable{cursor:pointer;}#mermaid-svg-XYws95RgjRhaOM4s .arrowheadPath{fill:#333333;}#mermaid-svg-XYws95RgjRhaOM4s .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-XYws95RgjRhaOM4s .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-XYws95RgjRhaOM4s .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-XYws95RgjRhaOM4s .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-XYws95RgjRhaOM4s .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-XYws95RgjRhaOM4s .cluster text{fill:#333;}#mermaid-svg-XYws95RgjRhaOM4s .cluster span{color:#333;}#mermaid-svg-XYws95RgjRhaOM4s div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-XYws95RgjRhaOM4s :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}哲学家1左手叉子1右手叉子2哲学家2左手叉子2右手叉子3哲学家3左手叉子3右手叉子4哲学家4左手叉子4右手叉子5哲学家5左手叉子5右手叉子1

问题描述:

  • 5个哲学家围坐圆桌,每人面前一个盘子
  • 相邻两人之间有一支叉子,共5支
  • 每个哲学家需要同时拿到左右两支叉子才能吃饭
  • 如果每个人都先拿左手叉子,再拿右手叉子,就会死锁

死锁分析:

时刻T1:所有哲学家同时拿起左手叉子
– 哲学家1拿叉子1 ✅
– 哲学家2拿叉子2 ✅
– 哲学家3拿叉子3 ✅
– 哲学家4拿叉子4 ✅
– 哲学家5拿叉子5 ✅

时刻T2:所有哲学家尝试拿右手叉子
– 哲学家1等待叉子2(被哲学家2占用)❌
– 哲学家2等待叉子3(被哲学家3占用)❌
– 哲学家3等待叉子4(被哲学家4占用)❌
– 哲学家4等待叉子5(被哲学家5占用)❌
– 哲学家5等待叉子1(被哲学家1占用)❌

结果:形成循环等待,死锁发生!

案例2:银行家转账系统

业务场景:银行转账系统中的死锁

// 账户类
class Account {
private int balance;
private final Object lock = new Object();

public void transfer(Account to, int amount) {
synchronized(this.lock) { // 锁定转出账户
synchronized(to.lock) { // 锁定转入账户
this.balance -= amount;
to.balance += amount;
}
}
}
}

// 死锁场景
Account accountA = new Account(1000);
Account accountB = new Account(1000);

// 线程1:A转账给B
new Thread(() -> {
accountA.transfer(accountB, 100);
}).start();

// 线程2:B转账给A
new Thread(() -> {
accountB.transfer(accountA, 200);
}).start();

死锁分析:

线程1执行流程:
1. 获得accountA.lock ✅
2. 尝试获得accountB.lock ❌ (被线程2占用)

线程2执行流程:
1. 获得accountB.lock ✅
2. 尝试获得accountA.lock ❌ (被线程1占用)

形成循环等待:线程1等线程2,线程2等线程1

案例3:数据库死锁

SQL死锁场景:

— 会话1
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance 100 WHERE id = 1; — 锁定账户1
— 此时等待…
UPDATE accounts SET balance = balance + 100 WHERE id = 2; — 尝试锁定账户2

— 会话2(同时执行)
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance 200 WHERE id = 2; — 锁定账户2
— 此时等待…
UPDATE accounts SET balance = balance + 200 WHERE id = 1; — 尝试锁定账户1

死锁时序图:

#mermaid-svg-n23yXXp7gglFbQoq {font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-n23yXXp7gglFbQoq .error-icon{fill:#552222;}#mermaid-svg-n23yXXp7gglFbQoq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-n23yXXp7gglFbQoq .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-n23yXXp7gglFbQoq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-n23yXXp7gglFbQoq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-n23yXXp7gglFbQoq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-n23yXXp7gglFbQoq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-n23yXXp7gglFbQoq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-n23yXXp7gglFbQoq .marker.cross{stroke:#333333;}#mermaid-svg-n23yXXp7gglFbQoq svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-n23yXXp7gglFbQoq .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-n23yXXp7gglFbQoq text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-n23yXXp7gglFbQoq .actor-line{stroke:grey;}#mermaid-svg-n23yXXp7gglFbQoq .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-n23yXXp7gglFbQoq .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-n23yXXp7gglFbQoq #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-n23yXXp7gglFbQoq .sequenceNumber{fill:white;}#mermaid-svg-n23yXXp7gglFbQoq #sequencenumber{fill:#333;}#mermaid-svg-n23yXXp7gglFbQoq #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-n23yXXp7gglFbQoq .messageText{fill:#333;stroke:#333;}#mermaid-svg-n23yXXp7gglFbQoq .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-n23yXXp7gglFbQoq .labelText,#mermaid-svg-n23yXXp7gglFbQoq .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-n23yXXp7gglFbQoq .loopText,#mermaid-svg-n23yXXp7gglFbQoq .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-n23yXXp7gglFbQoq .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-n23yXXp7gglFbQoq .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-n23yXXp7gglFbQoq .noteText,#mermaid-svg-n23yXXp7gglFbQoq .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-n23yXXp7gglFbQoq .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-n23yXXp7gglFbQoq .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-n23yXXp7gglFbQoq .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-n23yXXp7gglFbQoq .actorPopupMenu{position:absolute;}#mermaid-svg-n23yXXp7gglFbQoq .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-n23yXXp7gglFbQoq .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-n23yXXp7gglFbQoq .actor-man circle,#mermaid-svg-n23yXXp7gglFbQoq line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-n23yXXp7gglFbQoq :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}会话1数据库会话2锁定账户1账户1被锁定锁定账户2账户2被锁定请求锁定账户2等待中…请求锁定账户1等待中…检测到死锁!回滚事务2事务1继续执行会话1数据库会话2

🔧 死锁检测算法

资源分配图法

核心思想:将系统建模为有向图,检测是否存在环

#mermaid-svg-MHBMzBsagbvQso5N {font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-MHBMzBsagbvQso5N .error-icon{fill:#552222;}#mermaid-svg-MHBMzBsagbvQso5N .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MHBMzBsagbvQso5N .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-MHBMzBsagbvQso5N .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MHBMzBsagbvQso5N .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MHBMzBsagbvQso5N .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MHBMzBsagbvQso5N .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MHBMzBsagbvQso5N .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MHBMzBsagbvQso5N .marker.cross{stroke:#333333;}#mermaid-svg-MHBMzBsagbvQso5N svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MHBMzBsagbvQso5N .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-MHBMzBsagbvQso5N .cluster-label text{fill:#333;}#mermaid-svg-MHBMzBsagbvQso5N .cluster-label span{color:#333;}#mermaid-svg-MHBMzBsagbvQso5N .label text,#mermaid-svg-MHBMzBsagbvQso5N span{fill:#333;color:#333;}#mermaid-svg-MHBMzBsagbvQso5N .node rect,#mermaid-svg-MHBMzBsagbvQso5N .node circle,#mermaid-svg-MHBMzBsagbvQso5N .node ellipse,#mermaid-svg-MHBMzBsagbvQso5N .node polygon,#mermaid-svg-MHBMzBsagbvQso5N .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-MHBMzBsagbvQso5N .node .label{text-align:center;}#mermaid-svg-MHBMzBsagbvQso5N .node.clickable{cursor:pointer;}#mermaid-svg-MHBMzBsagbvQso5N .arrowheadPath{fill:#333333;}#mermaid-svg-MHBMzBsagbvQso5N .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-MHBMzBsagbvQso5N .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-MHBMzBsagbvQso5N .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-MHBMzBsagbvQso5N .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-MHBMzBsagbvQso5N .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-MHBMzBsagbvQso5N .cluster text{fill:#333;}#mermaid-svg-MHBMzBsagbvQso5N .cluster span{color:#333;}#mermaid-svg-MHBMzBsagbvQso5N div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-MHBMzBsagbvQso5N :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}资源进程资源R1资源R2资源R3进程P1进程P2进程P3

图的含义:

  • 实线箭头:资源分配关系(资源 → 进程)
  • 虚线箭头:资源请求关系(进程 → 资源)
  • 存在环路:P1 → R2 → P2 → R3 → P3 → R1 → P1,表示死锁

等待图检测算法

算法步骤:

1. 构建等待图:
– 节点代表进程
– 边代表等待关系(P1等待P2持有的资源)

2. 深度优先搜索检测环:
def has_cycle(graph):
visited = set()
rec_stack = set()

for node in graph:
if dfs_has_cycle(node, visited, rec_stack):
return True
return False

3. 如果发现环,则存在死锁

🛡️ 死锁预防策略

死锁预防的核心思想是破坏死锁产生的四个必要条件之一。

1. 破坏互斥条件

策略:尽可能让资源可共享

✅ 可行的改进:
– 只读文件:允许多进程同时读取
– 虚拟化技术:将独占资源虚拟化为可共享
– 资源池化:使用连接池代替独占连接

❌ 不可行的场景:
– 打印机:物理上无法同时使用
– 文件写操作:数据一致性要求独占
– 硬件设备:物理限制无法共享

2. 破坏占有和等待条件

策略:要求进程一次性申请所有需要的资源

// 改进前:可能死锁的代码
public void badTransfer(Account from, Account to, int amount) {
synchronized(from) { // 先占有from
Thread.sleep(100); // 模拟一些处理
synchronized(to) { // 再申请to,可能死锁
from.withdraw(amount);
to.deposit(amount);
}
}
}

// 改进后:一次性获取所有资源
public void goodTransfer(Account from, Account to, int amount) {
// 定义获取多个锁的顺序,避免死锁
Account firstLock = System.identityHashCode(from) < System.identityHashCode(to) ? from : to;
Account secondLock = System.identityHashCode(from) < System.identityHashCode(to) ? to : from;

synchronized(firstLock) {
synchronized(secondLock) {
from.withdraw(amount);
to.deposit(amount);
}
}
}

优缺点分析:

✅ 优点:
– 彻底避免死锁
– 逻辑简单清晰

❌ 缺点:
– 资源利用率低(提前占用暂不需要的资源)
– 可能导致饥饿(某些进程总是申请不到全部资源)
– 难以预知进程需要的全部资源

3. 破坏不可剥夺条件

策略:允许系统强制回收资源

// 使用tryLock实现可剥夺的锁
public boolean tryTransfer(Account from, Account to, int amount, long timeout) {
if (from.lock.tryLock(timeout, TimeUnit.MILLISECONDS)) {
try {
if (to.lock.tryLock(timeout, TimeUnit.MILLISECONDS)) {
try {
from.withdraw(amount);
to.deposit(amount);
return true;
} finally {
to.lock.unlock();
}
}
} finally {
from.lock.unlock();
}
}
return false; // 超时失败,避免死锁
}

4. 破坏循环等待条件

策略:对所有资源进行排序,要求进程按顺序申请资源

// 银行转账的正确实现:按账户ID排序获取锁
public void orderedTransfer(Account from, Account to, int amount) {
Account firstAccount = from.getId() < to.getId() ? from : to;
Account secondAccount = from.getId() < to.getId() ? to : from;

synchronized(firstAccount) {
synchronized(secondAccount) {
if (from == firstAccount) {
from.withdraw(amount);
to.deposit(amount);
} else {
from.withdraw(amount);
to.deposit(amount);
}
}
}
}

排序策略对比:

排序依据优点缺点适用场景
对象ID 简单可靠 需要全局唯一ID 数据库事务
内存地址 无需额外ID 地址可能变化 简单应用
哈希值 分布均匀 可能冲突 大规模系统
业务逻辑 符合直觉 复杂性高 特定业务

🏥 死锁避免:银行家算法

算法核心思想

银行家算法通过预先检查资源分配的安全性来避免死锁,确保系统始终处于安全状态。

在这里插入图片描述

算法实现示例

场景设置:

  • 3个进程:P0, P1, P2
  • 3种资源:A(10个), B(5个), C(7个)

当前资源分配矩阵 Allocation:
A B C
P0 0 1 0
P1 2 0 0
P2 3 0 2

最大需求矩阵 Max:
A B C
P0 7 5 3
P1 3 2 2
P2 9 0 2

还需资源矩阵 Need = Max – Allocation:
A B C
P0 7 4 3
P1 1 2 2
P2 6 0 0

当前可用资源 Available:
A: 10-0-2-3 = 5
B: 5-1-0-0 = 4
C: 7-0-0-2 = 5
Available = [5, 4, 5]

安全性检查算法:

def is_safe_state(allocation, max_need, available):
n_processes = len(allocation)
n_resources = len(available)

# 计算Need矩阵
need = [[max_need[i][j] allocation[i][j]
for j in range(n_resources)]
for i in range(n_processes)]

# 工作向量,初始等于可用资源
work = available[:]
finish = [False] * n_processes
safe_sequence = []

while len(safe_sequence) < n_processes:
found = False

for i in range(n_processes):
if not finish[i]:
# 检查Need[i] <= Work
if all(need[i][j] <= work[j] for j in range(n_resources)):
# 可以满足进程i的需求
for j in range(n_resources):
work[j] += allocation[i][j] # 回收资源

finish[i] = True
safe_sequence.append(i)
found = True
break

if not found:
return False, [] # 不安全状态

return True, safe_sequence # 安全状态

执行过程演示:

初始状态:Available = [5, 4, 5]

第1轮:
– 检查P0:Need[0] = [7,4,3] > Available[5,4,5] ❌
– 检查P1:Need[1] = [1,2,2] <= Available[5,4,5] ✅
执行P1,回收资源:Available = [5+2, 4+0, 5+0] = [7,4,5]

第2轮:
– 检查P0:Need[0] = [7,4,3] <= Available[7,4,5] ✅
执行P0,回收资源:Available = [7+0, 4+1, 5+0] = [7,5,5]

第3轮:
– 检查P2:Need[2] = [6,0,0] <= Available[7,5,5] ✅
执行P2,回收资源:Available = [7+3, 5+0, 5+2] = [10,5,7]

安全序列:P1 → P0 → P2
结论:当前状态安全,可以继续分配资源

🩺 死锁检测与恢复

检测机制

定期检测:

public class DeadlockDetector {
private final ScheduledExecutorService scheduler =
Executors.newScheduledThreadPool(1);

public void startDetection() {
scheduler.scheduleAtFixedRate(() -> {
if (detectDeadlock()) {
handleDeadlock();
}
}, 0, 5, TimeUnit.SECONDS); // 每5秒检测一次
}

private boolean detectDeadlock() {
// 构建等待图
Map<Thread, Set<Thread>> waitGraph = buildWaitGraph();

// 检测环路
return hasCycle(waitGraph);
}
}

实时检测:

// 在每次资源分配时检测
public synchronized boolean allocateResource(Process process, Resource resource) {
// 尝试分配
resource.allocateTo(process);

// 立即检测死锁
if (detectDeadlock()) {
// 回滚分配
resource.deallocateFrom(process);
return false;
}

return true;
}

恢复策略

1. 进程终止法

终止所有死锁进程:

public void terminateAllDeadlockedProcesses(Set<Process> deadlockedProcesses) {
for (Process process : deadlockedProcesses) {
process.terminate();
releaseAllResources(process);
log.info("终止死锁进程: " + process.getId());
}
}

逐个终止进程:

public void terminateProcessesOneByOne(Set<Process> deadlockedProcesses) {
while (detectDeadlock()) {
Process victim = selectVictim(deadlockedProcesses);
victim.terminate();
releaseAllResources(victim);
deadlockedProcesses.remove(victim);

log.info("终止进程: " + victim.getId() +
", 剩余死锁进程: " + deadlockedProcesses.size());
}
}

private Process selectVictim(Set<Process> processes) {
// 选择代价最小的进程
return processes.stream()
.min(Comparator.comparingInt(this::calculateTerminationCost))
.orElse(null);
}

private int calculateTerminationCost(Process process) {
return process.getExecutionTime() + // 已执行时间
process.getAllocatedResources().size() + // 占用资源数
process.getPriority(); // 优先级权重
}

2. 资源剥夺法

public void resolveByResourcePreemption(Set<Process> deadlockedProcesses) {
while (detectDeadlock()) {
// 选择被剥夺者
Process victim = selectPreemptionVictim(deadlockedProcesses);

// 剥夺部分资源
Set<Resource> preemptedResources = preemptResources(victim);

// 回滚进程到安全状态
victim.rollbackToSafePoint();

// 重新分配资源给等待的进程
redistributeResources(preemptedResources);

log.info("从进程 " + victim.getId() + " 剥夺了 " +
preemptedResources.size() + " 个资源");
}
}

🎯 面试高频问题解析

Q1: 请描述死锁的四个必要条件,并举例说明

回答模板:

死锁的四个必要条件是:

1. 互斥条件:资源不能被共享
例子:打印机同时只能被一个程序使用

2. 占有和等待:进程占有资源的同时等待其他资源
例子:线程A拿着锁1等待锁2,线程B拿着锁2等待锁1

3. 不可剥夺:资源不能被强制回收
例子:文件锁必须由持有者主动释放

4. 循环等待:进程间形成环形等待链
例子:P1等P2,P2等P3,P3等P1

这四个条件必须同时满足才会发生死锁,破坏任意一个就能避免死锁。

Q2: 如何在代码中避免死锁?

实用技巧总结:

// 技巧1:按顺序获取锁
public void orderedLocking() {
Object lock1 = getLock1();
Object lock2 = getLock2();

// 始终按相同顺序获取锁
Object first = System.identityHashCode(lock1) < System.identityHashCode(lock2) ? lock1 : lock2;
Object second = System.identityHashCode(lock1) < System.identityHashCode(lock2) ? lock2 : lock1;

synchronized(first) {
synchronized(second) {
// 业务逻辑
}
}
}

// 技巧2:使用超时锁
public boolean timeoutLocking() {
if (lock1.tryLock(1000, TimeUnit.MILLISECONDS)) {
try {
if (lock2.tryLock(1000, TimeUnit.MILLISECONDS)) {
try {
// 业务逻辑
return true;
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
return false; // 避免了死锁
}

// 技巧3:减少锁的范围
public void minimizeLockScope() {
// 预处理(不需要锁)
Data data = prepareData();

synchronized(lock) {
// 最小化临界区
updateSharedResource(data);
}

// 后处理(不需要锁)
processResult(data);
}

Q3: 数据库中如何处理死锁?

数据库死锁处理机制:

— 死锁检测示例
— 会话1
BEGIN;
UPDATE accounts SET balance = balance 100 WHERE id = 1;
— 等待2秒模拟业务处理
SELECT SLEEP(2);
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;

— 会话2(同时执行)
BEGIN;
UPDATE accounts SET balance = balance 50 WHERE id = 2;
— 等待2秒模拟业务处理
SELECT SLEEP(2);
UPDATE accounts SET balance = balance + 50 WHERE id = 1;
COMMIT;

— 数据库会自动检测死锁并回滚其中一个事务

避免数据库死锁的策略:

— 策略1:按固定顺序访问表和行
— 好的做法:始终按ID顺序更新
UPDATE accounts SET balance = balance 100
WHERE id IN (1, 2)
ORDER BY id;

— 策略2:使用适当的索引减少锁范围
CREATE INDEX idx_account_id ON accounts(id);

— 策略3:缩短事务时间
BEGIN;
— 尽快完成所有操作
UPDATE accounts SET balance = balance 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;

— 策略4:使用SELECT … FOR UPDATE显式加锁
BEGIN;
SELECT * FROM accounts WHERE id IN (1, 2) ORDER BY id FOR UPDATE;
— 现在可以安全地更新
UPDATE accounts SET balance = balance 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;

Q4: 分布式系统中的死锁如何处理?

分布式死锁的特点:

  • 跨节点的资源依赖
  • 检测复杂度高
  • 网络延迟影响

解决方案:

// 分布式锁的正确实现
public class DistributedLockManager {
private final RedisTemplate<String, String> redisTemplate;

public boolean tryLock(String lockKey, String clientId, long expireTime) {
// 使用SET NX EX原子操作
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(lockKey, clientId, Duration.ofMillis(expireTime));
return Boolean.TRUE.equals(result);
}

public void unlock(String lockKey, String clientId) {
// 使用Lua脚本确保原子性
String script = """
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
"""
;
redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
Arrays.asList(lockKey), clientId);
}
}

// 分布式死锁避免:使用超时机制
public boolean distributedTransfer(String fromAccount, String toAccount,
BigDecimal amount) {
String lockKey1 = "account:lock:" + fromAccount;
String lockKey2 = "account:lock:" + toAccount;
String clientId = UUID.randomUUID().toString();

// 按字典序获取锁,避免死锁
String firstLock = lockKey1.compareTo(lockKey2) < 0 ? lockKey1 : lockKey2;
String secondLock = lockKey1.compareTo(lockKey2) < 0 ? lockKey2 : lockKey1;

try {
// 获取第一个锁
if (!tryLock(firstLock, clientId, 5000)) {
return false;
}

// 获取第二个锁
if (!tryLock(secondLock, clientId, 5000)) {
unlock(firstLock, clientId);
return false;
}

// 执行转账业务
performTransfer(fromAccount, toAccount, amount);
return true;

} finally {
unlock(secondLock, clientId);
unlock(firstLock, clientId);
}
}

🛠️ 实际项目中的死锁解决案例

案例1:电商系统订单处理死锁

问题背景:
电商系统在处理订单时,需要同时锁定用户账户、商品库存和优惠券,高并发下经常发生死锁。

原始代码问题:

// 有问题的实现
public void createOrder(Long userId, Long productId, Long couponId) {
synchronized(getUserLock(userId)) { // 锁用户
synchronized(getProductLock(productId)) { // 锁商品
synchronized(getCouponLock(couponId)) { // 锁优惠券
// 创建订单逻辑
User user = userService.getUser(userId);
Product product = productService.getProduct(productId);
Coupon coupon = couponService.getCoupon(couponId);

orderService.createOrder(user, product, coupon);
}
}
}
}

死锁场景:

用户A购买商品1,使用优惠券X:锁顺序 A → 1 → X
用户B购买商品2,使用优惠券Y:锁顺序 B → 2 → Y
用户A购买商品2,使用优惠券Y:锁顺序 A → 2 → Y
用户B购买商品1,使用优惠券X:锁顺序 B → 1 → X

交叉执行时可能出现:
A锁定用户A,B锁定用户B
A等待商品1(被B锁定),B等待商品2(被A锁定)

解决方案:

public class OrderService {

// 解决方案1:全局排序策略
public boolean createOrderWithGlobalOrdering(Long userId, Long productId, Long couponId) {
// 按ID大小排序,确保全局一致的加锁顺序
List<Lockable> resources = Arrays.asList(
new UserLock(userId),
new ProductLock(productId),
new CouponLock(couponId)
);

resources.sort(Comparator.comparing(Lockable::getLockId));

return executeWithOrderedLocks(resources, () -> {
User user = userService.getUser(userId);
Product product = productService.getProduct(productId);
Coupon coupon = couponService.getCoupon(couponId);

return orderService.createOrder(user, product, coupon);
});
}

// 解决方案2:超时重试策略
public boolean createOrderWithTimeout(Long userId, Long productId, Long couponId) {
int maxRetries = 3;
for (int attempt = 0; attempt < maxRetries; attempt++) {
try {
if (tryCreateOrderWithTimeout(userId, productId, couponId, 1000)) {
return true;
}
// 随机等待时间,避免活锁
Thread.sleep(Random.nextInt(100));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return false;
}

private boolean tryCreateOrderWithTimeout(Long userId, Long productId,
Long couponId, long timeoutMs) {
ReentrantLock userLock = getLockManager().getUserLock(userId);
ReentrantLock productLock = getLockManager().getProductLock(productId);
ReentrantLock couponLock = getLockManager().getCouponLock(couponId);

try {
if (userLock.tryLock(timeoutMs, TimeUnit.MILLISECONDS)) {
try {
if (productLock.tryLock(timeoutMs, TimeUnit.MILLISECONDS)) {
try {
if (couponLock.tryLock(timeoutMs, TimeUnit.MILLISECONDS)) {
try {
// 执行订单创建逻辑
return performOrderCreation(userId, productId, couponId);
} finally {
couponLock.unlock();
}
}
} finally {
productLock.unlock();
}
}
} finally {
userLock.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return false;
}
}

案例2:数据库连接池死锁

问题描述:
应用使用数据库连接池,在事务中需要获取多个连接时发生死锁。

死锁场景:

// 事务1:转账操作
@Transactional
public void transfer(String fromAccount, String toAccount, BigDecimal amount) {
Connection conn1 = dataSource.getConnection(); // 获取连接1
// 查询转出账户
Account from = accountDao.findById(fromAccount, conn1);

Connection conn2 = dataSource.getConnection(); // 获取连接2
// 查询转入账户
Account to = accountDao.findById(toAccount, conn2);

// 更新操作…
}

// 事务2:余额查询
@Transactional
public void queryBalance(String account1, String account2) {
Connection conn1 = dataSource.getConnection(); // 获取连接1
// 查询账户1

Connection conn2 = dataSource.getConnection(); // 获取连接2
// 查询账户2
}

问题分析:

  • 连接池大小有限(如最大10个连接)
  • 高并发时连接池耗尽
  • 事务A占用连接等待更多连接,事务B也在等待

解决方案:

public class OptimizedAccountService {

// 解决方案1:单连接事务
@Transactional
public void transferWithSingleConnection(String fromAccount, String toAccount,
BigDecimal amount) {
// 在同一个事务中使用同一个连接
Account from = accountDao.findById(fromAccount); // 使用事务连接
Account to = accountDao.findById(toAccount); // 使用事务连接

// 验证余额
if (from.getBalance().compareTo(amount) < 0) {
throw new InsufficientBalanceException();
}

// 更新余额
from.setBalance(from.getBalance().subtract(amount));
to.setBalance(to.getBalance().add(amount));

accountDao.update(from);
accountDao.update(to);
}

// 解决方案2:连接池监控和预警
@Component
public class ConnectionPoolMonitor {

@Scheduled(fixedRate = 5000) // 每5秒检查一次
public void monitorConnectionPool() {
HikariDataSource dataSource = (HikariDataSource) this.dataSource;
HikariPoolMXBean poolBean = dataSource.getHikariPoolMXBean();

int activeConnections = poolBean.getActiveConnections();
int totalConnections = poolBean.getTotalConnections();
int maxPoolSize = dataSource.getMaximumPoolSize();

double usage = (double) activeConnections / maxPoolSize;

if (usage > 0.8) { // 使用率超过80%预警
log.warn("连接池使用率过高: {}/{} ({}%)",
activeConnections, maxPoolSize,
String.format("%.1f", usage * 100));

// 可以触发扩容或限流措施
triggerConnectionPoolAlert(usage);
}
}
}

// 解决方案3:异步处理 + 队列
@Service
public class AsyncTransferService {

@Async("transferExecutor")
public CompletableFuture<Boolean> asyncTransfer(TransferRequest request) {
try {
// 使用消息队列解耦,避免长时间占用连接
transferQueue.send(request);
return CompletableFuture.completedFuture(true);
} catch (Exception e) {
return CompletableFuture.completedFuture(false);
}
}

@RabbitListener(queues = "transfer.queue")
public void processTransfer(TransferRequest request) {
// 在专门的消费者线程中处理,避免死锁
transferWithSingleConnection(request.getFromAccount(),
request.getToAccount(),
request.getAmount());
}
}
}

🔍 死锁诊断工具和技巧

Java应用死锁诊断

// 1. 使用ThreadMXBean检测死锁
public class DeadlockDetector {
private final ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();

public void detectDeadlock() {
long[] deadlockedThreads = threadBean.findDeadlockedThreads();

if (deadlockedThreads != null) {
ThreadInfo[] threadInfos = threadBean.getThreadInfo(deadlockedThreads);

System.out.println("检测到死锁!涉及线程:");
for (ThreadInfo threadInfo : threadInfos) {
System.out.println("线程: " + threadInfo.getThreadName());
System.out.println("状态: " + threadInfo.getThreadState());
System.out.println("锁信息: " + threadInfo.getLockInfo());
System.out.println("等待的锁: " + threadInfo.getLockName());
System.out.println("锁拥有者: " + threadInfo.getLockOwnerName());

// 打印堆栈跟踪
StackTraceElement[] stackTrace = threadInfo.getStackTrace();
for (StackTraceElement element : stackTrace) {
System.out.println("\\tat " + element);
}
System.out.println();
}
}
}
}

// 2. JMX监控死锁
@Component
public class DeadlockMonitor {

@EventListener
@Async
public void monitorDeadlock() {
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);

executor.scheduleAtFixedRate(() -> {
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();

if (deadlockedThreads != null) {
// 发送告警
alertService.sendDeadlockAlert(deadlockedThreads);

// 记录详细信息
logDeadlockDetails(threadMXBean, deadlockedThreads);

// 可选:自动重启应用
// applicationContext.close();
}
}, 10, 10, TimeUnit.SECONDS);
}
}

命令行诊断工具

# 1. jstack 查看线程堆栈
jstack <pid> | grep -A 5 -B 5 "BLOCKED"

# 2. jconsole 图形化监控
jconsole <pid>

# 3. jvisualvm 可视化分析
jvisualvm –jdkhome $JAVA_HOME

# 4. arthas 动态诊断(推荐)
java -jar arthas-boot.jar <pid>
# 在arthas中执行:
thread -b # 查看阻塞的线程
thread -n 3 # 查看CPU使用率最高的3个线程

数据库死锁诊断

— MySQL死锁诊断
SHOW ENGINE INNODB STATUS;

— 查看当前锁等待
SELECT
r.trx_id waiting_trx_id,
r.trx_mysql_thread_id waiting_thread,
r.trx_query waiting_query,
b.trx_id blocking_trx_id,
b.trx_mysql_thread_id blocking_thread,
b.trx_query blocking_query
FROM information_schema.innodb_lock_waits w
INNER JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
INNER JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id;

— PostgreSQL死锁诊断
SELECT
blocked_locks.pid AS blocked_pid,
blocked_activity.usename AS blocked_user,
blocking_locks.pid AS blocking_pid,
blocking_activity.usename AS blocking_user,
blocked_activity.query AS blocked_statement,
blocking_activity.query AS current_statement_in_blocking_process
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_stat_activity blocked_activity
ON blocked_activity.pid = blocked_locks.pid
JOIN pg_catalog.pg_locks blocking_locks
ON blocking_locks.locktype = blocked_locks.locktype
AND blocking_locks.DATABASE IS NOT DISTINCT FROM blocked_locks.DATABASE
AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
JOIN pg_catalog.pg_stat_activity blocking_activity
ON blocking_activity.pid = blocking_locks.pid
WHERE NOT blocked_locks.GRANTED;

🎓 面试总结和记忆技巧

死锁知识点思维导图

在这里插入图片描述

面试金句总结

  • “死锁的本质是循环等待,解决的关键是打破循环”

  • “预防胜于治疗,代码设计时就要考虑锁的顺序”

  • “超时机制是工程实践中最常用的死锁避免手段”

  • “死锁检测的代价通常比死锁本身的损失更大”

  • “分布式系统中的死锁更难检测,设计时就要避免”

  • 快速记忆口诀

    死锁四条件,缺一不成锁
    互斥占等待,剥夺循环导
    预防破条件,避免用银行
    检测画等图,恢复靠终止

    常见面试陷阱

    陷阱1:只记住理论,不会实际应用

    ❌ 错误回答:"死锁就是互相等待"
    ✅ 正确回答:"死锁是循环等待,在实际开发中我们通过锁排序来避免,
    比如在转账系统中按账户ID顺序获取锁…"

    陷阱2:混淆死锁和活锁

    死锁:进程都停止,互相等待
    活锁:进程都在运行,但无法前进(如两人在走廊相遇,总是同时让路)

    陷阱3:认为死锁检测很简单

    实际上死锁检测在大型系统中非常复杂:
    – 分布式环境下的全局检测
    – 检测的性能开销
    – 误判的处理
    – 网络分区的影响

    🚀 进阶思考

    现代系统中的死锁新挑战

  • 微服务架构:跨服务的资源依赖
  • 云原生应用:容器间的资源竞争
  • 大数据系统:分布式锁的性能瓶颈
  • 区块链:共识算法中的死锁避免
  • 未来发展趋势

  • AI辅助检测:机器学习预测死锁风险
  • 自适应算法:根据系统负载动态调整策略
  • 硬件支持:CPU级别的死锁检测
  • 形式化验证:数学方法证明代码无死锁

  • 💡 面试提示:死锁问题不仅考查理论知识,更重要的是实际问题解决能力。准备时要结合具体项目经验,能够描述遇到的实际死锁问题和解决方案,这样更容易打动面试官!

    记住:理解死锁不是为了考试,而是为了写出更好的并发程序。在实际工作中,预防死锁的设计思维比事后解决更有价值!

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 死锁问题面试全攻略
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!