死锁
面试高频考点:死锁是多线程编程中最常见的问题之一,掌握死锁的产生条件、检测方法和预防策略是面试必考内容。
核心概念
什么是死锁
死锁(Deadlock) 是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象。如果没有外力干涉,它们都将无法继续执行下去。
线程A: 持有资源1 → 等待资源2
线程B: 持有资源2 → 等待资源1
结果: 互相等待,永远无法完成死锁的四个必要条件
死锁发生必须同时满足以下四个条件(缺一不可):
| 条件 | 描述 | 说明 |
|---|---|---|
| 互斥条件 | 资源只能被一个线程占用 | 资源具有排他性 |
| 请求与保持条件 | 线程持有资源同时请求新资源 | 不释放已持有的资源 |
| 不剥夺条件 | 线程已获得的资源不能被强制剥夺 | 只能主动释放 |
| 循环等待条件 | 存在循环等待资源的关系 | A等B,B等A |
死锁示例代码
经典死锁示例
public class DeadlockDemo {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) {
// 线程1:先获取lockA,再获取lockB
Thread thread1 = new Thread(() -> {
synchronized (lockA) {
System.out.println("Thread1: 持有lockA");
try {
Thread.sleep(100); // 让线程2有机会获取lockB
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread1: 等待lockB");
synchronized (lockB) {
System.out.println("Thread1: 持有lockA和lockB");
}
}
});
// 线程2:先获取lockB,再获取lockA
Thread thread2 = new Thread(() -> {
synchronized (lockB) {
System.out.println("Thread2: 持有lockB");
try {
Thread.sleep(100); // 让线程1有机会获取lockA
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread2: 等待lockA");
synchronized (lockA) {
System.out.println("Thread2: 持有lockA和lockB");
}
}
});
thread1.start();
thread2.start();
// 程序将卡住,无法继续执行
}
}输出结果:
Thread1: 持有lockA
Thread2: 持有lockB
Thread1: 等待lockB
Thread2: 等待lockA
// 程序卡住,形成死锁银行转账死锁示例
public class Account {
private int balance;
private final String name;
public Account(String name, int balance) {
this.name = name;
this.balance = balance;
}
public void transfer(Account target, int amount) {
synchronized (this) { // 锁定转出账户
synchronized (target) { // 锁定转入账户
if (this.balance >= amount) {
this.balance -= amount;
target.balance += amount;
}
}
}
}
}
// 死锁场景
Account accountA = new Account("A", 1000);
Account accountB = new Account("B", 1000);
// 线程1:A转账给B
new Thread(() -> accountA.transfer(accountB, 100)).start();
// 线程2:B转账给A
new Thread(() -> accountB.transfer(accountA, 100)).start();
// 可能产生死锁:线程1持有A等B,线程2持有B等A死锁检测方法
使用 jstack 检测
# 1. 查找 Java 进程 ID
jps -l
# 输出示例:
# 12345 DeadlockDemo
# 12346 Jps
# 2. 使用 jstack 检测死锁
jstack 12345jstack 输出示例:
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x0000000012345678 (object 0x00000000d1234567, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x0000000012345690 (object 0x00000000d1234589, a java.lang.Object),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
at DeadlockDemo.lambda$main$1(DeadlockDemo.java:25)
- waiting to lock <0x00000000d1234567> (a java.lang.Object)
- locked <0x00000000d1234589> (a java.lang.Object)
"Thread-0":
at DeadlockDemo.lambda$main$0(DeadlockDemo.java:15)
- waiting to lock <0x00000000d1234589> (a java.lang.Object)
- locked <0x00000000d1234567> (a java.lang.Object)
Found 1 deadlock.使用 VisualVM 检测
- 启动 VisualVM:
jvisualvm或从 JDK bin 目录运行 - 选择目标 Java 进程
- 切换到"线程"标签页
- 点击"检测死锁"按钮
- 查看死锁详情和线程堆栈
使用 JConsole 检测
# 启动 JConsole
jconsole
# 连接到目标进程后,查看"线程"选项卡
# 死锁线程会显示为"阻塞"状态代码层面检测
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;
public class DeadlockDetector {
public static void detectDeadlock() {
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
if (deadlockedThreads != null) {
System.out.println("检测到死锁!");
ThreadInfo[] threadInfos = threadMXBean.getThreadInfo(deadlockedThreads);
for (ThreadInfo info : threadInfos) {
System.out.println("死锁线程: " + info.getThreadName());
System.out.println("线程状态: " + info.getThreadState());
System.out.println("等待锁: " + info.getLockName());
System.out.println("堆栈信息:");
for (StackTraceElement element : info.getStackTrace()) {
System.out.println(" " + element);
}
}
} else {
System.out.println("未检测到死锁");
}
}
}死锁预防策略
1. 破坏互斥条件
方法:使用无锁数据结构或乐观锁
// 使用 AtomicInteger 替代 synchronized
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // 无锁操作
// 使用 ConcurrentHashMap
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key", 1); // 内部使用分段锁或 CAS2. 破坏请求与保持条件
方法:一次性申请所有资源
public class ResourceAllocator {
private final Object lock = new Object();
// 一次性获取所有需要的锁
public void transfer(Account from, Account to, int amount) {
synchronized (lock) { // 先获取全局锁
synchronized (from) {
synchronized (to) {
from.debit(amount);
to.credit(amount);
}
}
}
}
}3. 破坏不剥夺条件
方法:使用 tryLock 设置超时
import java.util.concurrent.locks.ReentrantLock;
public class DeadlockPrevention {
private final ReentrantLock lock1 = new ReentrantLock();
private final ReentrantLock lock2 = new ReentrantLock();
public void method1() {
try {
if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
// 执行业务逻辑
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}4. 破坏循环等待条件
方法:按顺序获取锁
public class OrderedLockTransfer {
public static void transfer(Account from, Account to, int amount) {
// 按账户名称排序,确保锁获取顺序一致
Account first = from.getName().compareTo(to.getName()) < 0 ? from : to;
Account second = from.getName().compareTo(to.getName()) < 0 ? to : from;
synchronized (first) {
synchronized (second) {
from.debit(amount);
to.credit(amount);
}
}
}
}
// 或者使用 System.identityHashCode 排序
public static void transfer(Account from, Account to, int amount) {
int fromHash = System.identityHashCode(from);
int toHash = System.identityHashCode(to);
if (fromHash < toHash) {
synchronized (from) {
synchronized (to) {
// 转账逻辑
}
}
} else if (fromHash > toHash) {
synchronized (to) {
synchronized (from) {
// 转账逻辑
}
}
} else {
// 哈希冲突,使用第三个锁
synchronized (Account.class) {
synchronized (from) {
synchronized (to) {
// 转账逻辑
}
}
}
}
}死锁避免策略
银行家算法
银行家算法是一种经典的死锁避免算法,通过预先检查资源分配是否安全来避免死锁。
public class BankersAlgorithm {
private int[] available; // 可用资源
private int[][] max; // 最大需求矩阵
private int[][] allocation; // 已分配矩阵
private int[][] need; // 需求矩阵
public BankersAlgorithm(int[] available, int[][] max) {
this.available = available.clone();
this.max = max;
this.allocation = new int[max.length][available.length];
this.need = new int[max.length][available.length];
// 计算需求矩阵
for (int i = 0; i < max.length; i++) {
for (int j = 0; j < available.length; j++) {
need[i][j] = max[i][j];
}
}
}
// 检查是否可以安全分配
public boolean isSafe(int processId, int[] request) {
// 检查请求是否超过需求
for (int i = 0; i < request.length; i++) {
if (request[i] > need[processId][i]) {
return false;
}
}
// 检查请求是否超过可用资源
for (int i = 0; i < request.length; i++) {
if (request[i] > available[i]) {
return false;
}
}
// 试探性分配
for (int i = 0; i < request.length; i++) {
available[i] -= request[i];
allocation[processId][i] += request[i];
need[processId][i] -= request[i];
}
// 检查是否处于安全状态
boolean safe = checkSafe();
// 如果不安全,回滚
if (!safe) {
for (int i = 0; i < request.length; i++) {
available[i] += request[i];
allocation[processId][i] -= request[i];
need[processId][i] += request[i];
}
}
return safe;
}
private boolean checkSafe() {
// 实现安全性检查算法
// ...
return true;
}
}超时机制
public class TimeoutLock {
private final Lock lock1 = new ReentrantLock();
private final Lock lock2 = new ReentrantLock();
public void executeWithTimeout() {
boolean acquired1 = false;
boolean acquired2 = false;
try {
acquired1 = lock1.tryLock(1, TimeUnit.SECONDS);
if (acquired1) {
acquired2 = lock2.tryLock(1, TimeUnit.SECONDS);
if (acquired2) {
// 执行业务逻辑
doWork();
} else {
System.out.println("获取lock2超时,放弃操作");
}
} else {
System.out.println("获取lock1超时,放弃操作");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (acquired2) lock2.unlock();
if (acquired1) lock1.unlock();
}
}
private void doWork() {
// 业务逻辑
}
}死锁恢复
强制终止线程
// 注意:不推荐使用,可能导致数据不一致
thread.stop(); // 已废弃,危险操作剥夺资源
// 使用 ReentrantLock 的 interrupt 方法
ReentrantLock lock = new ReentrantLock();
// 线程1持有锁
Thread t1 = new Thread(() -> {
lock.lock();
try {
Thread.sleep(10000); // 长时间持有锁
} catch (InterruptedException e) {
System.out.println("线程被中断");
} finally {
lock.unlock();
}
});
// 线程2尝试获取锁
Thread t2 = new Thread(() -> {
try {
if (!lock.tryLock(5, TimeUnit.SECONDS)) {
t1.interrupt(); // 中断线程1
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});实际案例分析
数据库连接池死锁
// 问题场景:两个线程各自持有一个连接,等待对方释放连接
public class ConnectionPoolDeadlock {
private final List<Connection> pool = new ArrayList<>();
private final int maxSize = 10;
public synchronized Connection getConnection() throws InterruptedException {
while (pool.isEmpty() && pool.size() >= maxSize) {
wait(); // 等待连接释放
}
if (!pool.isEmpty()) {
return pool.remove(0);
}
return createConnection();
}
public synchronized void releaseConnection(Connection conn) {
pool.add(conn);
notifyAll();
}
private Connection createConnection() {
// 创建新连接
return null;
}
}
// 解决方案:设置获取连接的超时时间
public Connection getConnection(long timeout, TimeUnit unit)
throws InterruptedException, TimeoutException {
long deadline = System.nanoTime() + unit.toNanos(timeout);
synchronized (this) {
while (pool.isEmpty()) {
long remaining = deadline - System.nanoTime();
if (remaining <= 0) {
throw new TimeoutException("获取连接超时");
}
wait(remaining / 1_000_000, (int)(remaining % 1_000_000));
}
return pool.remove(0);
}
}面试要点
问题1
Q: 死锁产生的四个必要条件是什么? A:
- 互斥条件:资源具有排他性,同一时间只能被一个线程使用
- 请求与保持条件:线程持有资源的同时请求新资源
- 不剥夺条件:已获得的资源不能被强制剥夺
- 循环等待条件:存在循环等待资源的关系
这四个条件必须同时满足才会发生死锁,破坏任一条件即可预防死锁。
问题2
Q: 如何检测死锁? A: 主要方法有:
- jstack:
jstack <pid>命令会自动检测并报告死锁 - VisualVM:图形化工具,可以检测死锁并查看详情
- JConsole:线程选项卡可以查看死锁线程
- ThreadMXBean:代码层面使用
findDeadlockedThreads()方法
问题3
Q: 如何预防死锁? A: 破坏四个必要条件之一:
- 破坏互斥:使用无锁数据结构(如 AtomicInteger、CAS)
- 破坏请求与保持:一次性申请所有资源
- 破坏不剥夺:使用 tryLock 设置超时,获取失败则释放已持有资源
- 破坏循环等待:按固定顺序获取锁
问题4
Q: 银行转账如何避免死锁? A: 有几种方案:
- 按顺序加锁:根据账户 ID 或哈希值排序,确保锁获取顺序一致
- 使用全局锁:先获取全局锁,再获取账户锁(降低并发性)
- 使用 tryLock:设置超时,失败后重试
- 使用分布式锁:在分布式场景下使用 Redis 或 ZooKeeper 锁
问题5
Q: synchronized 和 ReentrantLock 在死锁处理上有什么区别? A:
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 获取锁方式 | 阻塞式,无法中断 | 支持 tryLock 和超时 |
| 释放锁 | 自动释放 | 必须手动 unlock |
| 死锁避免 | 难以避免 | 可以使用 tryLock 避免 |
| 公平性 | 非公平 | 支持公平/非公平 |
问题6
Q: 什么是活锁?与死锁有什么区别? A:
- 死锁:线程互相等待,无法继续执行(静止状态)
- 活锁:线程不断改变状态,但无法取得进展(忙碌状态)
活锁示例:
// 两个线程互相谦让,都无法获取资源
while (true) {
if (lock1.tryLock()) {
if (!lock2.tryLock()) {
lock1.unlock();
Thread.sleep(100); // 谦让
continue;
}
// 执行逻辑
}
}总结
| 策略 | 方法 | 适用场景 |
|---|---|---|
| 预防 | 破坏必要条件 | 设计阶段 |
| 避免 | 银行家算法、超时 | 运行时检查 |
| 检测 | jstack、VisualVM | 排查问题 |
| 恢复 | 终止线程、剥夺资源 | 紧急处理 |
最佳实践:
- 设计时考虑锁的顺序
- 尽量减少锁的持有时间
- 使用 tryLock 替代 lock
- 定期进行死锁检测
- 使用更高层次的并发工具
理解死锁原理是编写高质量并发代码的基础!