知识模块
☕ Java 知识模块
三、Java 并发编程
死锁

死锁

面试高频考点:死锁是多线程编程中最常见的问题之一,掌握死锁的产生条件、检测方法和预防策略是面试必考内容。

核心概念

什么是死锁

死锁(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 12345

jstack 输出示例

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 检测

  1. 启动 VisualVM:jvisualvm 或从 JDK bin 目录运行
  2. 选择目标 Java 进程
  3. 切换到"线程"标签页
  4. 点击"检测死锁"按钮
  5. 查看死锁详情和线程堆栈

使用 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);  // 内部使用分段锁或 CAS

2. 破坏请求与保持条件

方法:一次性申请所有资源

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:

  1. 互斥条件:资源具有排他性,同一时间只能被一个线程使用
  2. 请求与保持条件:线程持有资源的同时请求新资源
  3. 不剥夺条件:已获得的资源不能被强制剥夺
  4. 循环等待条件:存在循环等待资源的关系

这四个条件必须同时满足才会发生死锁,破坏任一条件即可预防死锁。

问题2

Q: 如何检测死锁? A: 主要方法有:

  1. jstackjstack <pid> 命令会自动检测并报告死锁
  2. VisualVM:图形化工具,可以检测死锁并查看详情
  3. JConsole:线程选项卡可以查看死锁线程
  4. ThreadMXBean:代码层面使用 findDeadlockedThreads() 方法

问题3

Q: 如何预防死锁? A: 破坏四个必要条件之一:

  1. 破坏互斥:使用无锁数据结构(如 AtomicInteger、CAS)
  2. 破坏请求与保持:一次性申请所有资源
  3. 破坏不剥夺:使用 tryLock 设置超时,获取失败则释放已持有资源
  4. 破坏循环等待:按固定顺序获取锁

问题4

Q: 银行转账如何避免死锁? A: 有几种方案:

  1. 按顺序加锁:根据账户 ID 或哈希值排序,确保锁获取顺序一致
  2. 使用全局锁:先获取全局锁,再获取账户锁(降低并发性)
  3. 使用 tryLock:设置超时,失败后重试
  4. 使用分布式锁:在分布式场景下使用 Redis 或 ZooKeeper 锁

问题5

Q: synchronized 和 ReentrantLock 在死锁处理上有什么区别? A:

特性synchronizedReentrantLock
获取锁方式阻塞式,无法中断支持 tryLock 和超时
释放锁自动释放必须手动 unlock
死锁避免难以避免可以使用 tryLock 避免
公平性非公平支持公平/非公平

问题6

Q: 什么是活锁?与死锁有什么区别? A:

  • 死锁:线程互相等待,无法继续执行(静止状态)
  • 活锁:线程不断改变状态,但无法取得进展(忙碌状态)

活锁示例:

// 两个线程互相谦让,都无法获取资源
while (true) {
    if (lock1.tryLock()) {
        if (!lock2.tryLock()) {
            lock1.unlock();
            Thread.sleep(100);  // 谦让
            continue;
        }
        // 执行逻辑
    }
}

总结

策略方法适用场景
预防破坏必要条件设计阶段
避免银行家算法、超时运行时检查
检测jstack、VisualVM排查问题
恢复终止线程、剥夺资源紧急处理

最佳实践

  1. 设计时考虑锁的顺序
  2. 尽量减少锁的持有时间
  3. 使用 tryLock 替代 lock
  4. 定期进行死锁检测
  5. 使用更高层次的并发工具

理解死锁原理是编写高质量并发代码的基础!