如何看待和解决死锁问题?


死锁是多线程并发中因资源竞争导致的永久阻塞状态,其产生需满足四个条件:

  • 互斥持有资源

  • 持有并等待新资源

  • 资源不可剥夺

  • 循环等待资源

    避免死锁的核心是打破这四个条件中的至少一个

    针对 synchronizedReentrantLock 的特性,可采用以下具体策略:

一、通用预防策略(适用于两种锁)

无论使用哪种锁,以下原则能从根本上减少死锁风险:

1. 固定锁的获取顺序(打破 “循环等待”)

死锁最常见的场景是 “线程 A 持有锁 1 并等待锁 2,线程 B 持有锁 2 并等待锁 1”(循环等待)。让所有线程按统一顺序获取锁,可彻底避免循环等待。

示例: 定义锁的 “优先级”(如按锁对象的哈希值或业务逻辑排序),所有线程必须先获取低优先级锁,再获取高优先级锁。

// 锁1和锁2,规定必须先获取lock1,再获取lock2
Object lock1 = new Object();
Object lock2 = new Object();

// 线程A:按顺序获取lock1→lock2
new Thread(() -> {
    synchronized (lock1) { // 先低优先级
        try { Thread.sleep(100); } catch (InterruptedException e) {}
        synchronized (lock2) { // 再高优先级
            System.out.println("线程A获取双锁成功");
        }
    }
}).start();

// 线程B:同样按顺序获取lock1→lock2(而非lock2→lock1)
new Thread(() -> {
    synchronized (lock1) { // 先低优先级
        try { Thread.sleep(100); } catch (InterruptedException e) {}
        synchronized (lock2) { // 再高优先级
            System.out.println("线程B获取双锁成功");
        }
    }
}).start();

原理:所有线程获取锁的顺序一致,不会出现 “交叉等待”,打破循环等待条件。

2. 减少锁的嵌套层级(降低 “持有并等待” 概率)

嵌套锁是死锁的高危场景(持有锁 A 时等待锁 B,持有锁 B 时可能等待其他锁)。尽量避免锁的嵌套,或减少嵌套层数,可降低死锁风险。

反例(危险)

// 嵌套锁导致死锁风险
synchronized (lockA) {
    // 业务逻辑
    synchronized (lockB) {
        // 嵌套逻辑
    }
}

优化: 将嵌套逻辑拆分为独立方法,通过 “一次性获取所有需要的锁” 或 “减少锁的范围” 避免嵌套。

3. 减少锁的持有时间(降低冲突窗口)

线程持有锁的时间越长,其他线程等待锁的时间就越长,死锁概率越高。在锁内部只执行核心逻辑,快速释放锁,可减少冲突窗口。

示例

synchronized (lock) {
    // 只做必须同步的操作(如修改共享变量)
    sharedVar = newValue; 
}
// 非核心逻辑(如IO、计算)放在锁外部
nonCriticalOperation(); 

4. 使用 try-finally 确保锁释放(避免 “资源不可剥夺”)

若线程获取锁后因异常退出而未释放锁,会导致其他线程永久等待(资源被永久持有)。需确保锁最终被释放:

  • synchronized 由 JVM 自动释放(无需手动处理)。
  • ReentrantLock 必须在 finally 中调用 unlock() 释放。

二、针对 ReentrantLock 的特有策略(利用其灵活性)

ReentrantLock 提供的 超时机制中断支持 是避免死锁的强大工具,可主动打破 “持有并等待” 和 “不可剥夺” 条件。

1. 使用 tryLock () 设置超时(主动放弃等待)

通过 tryLock(long timeout, TimeUnit unit) 尝试获取锁,若超时未获取则主动释放已持有的锁,避免永久阻塞。

示例: 线程需要获取锁 A 和锁 B,若获取锁 B 超时,则释放已持有的锁 A:

ReentrantLock lockA = new ReentrantLock();
ReentrantLock lockB = new ReentrantLock();

new Thread(() -> {
    boolean gotA = false;
    boolean gotB = false;
    try {
        // 先获取锁A
        gotA = lockA.tryLock(1, TimeUnit.SECONDS);
        if (!gotA) {
            System.out.println("线程1获取锁A失败,放弃");
            return;
        }
        
        // 再尝试获取锁B,超时1秒
        gotB = lockB.tryLock(1, TimeUnit.SECONDS);
        if (!gotB) {
            System.out.println("线程1获取锁B超时,释放锁A");
            return; // 未获取锁B,自动释放已持有的锁A(在finally中)
        }
        
        // 成功获取双锁,执行逻辑
        System.out.println("线程1获取双锁成功");
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    } finally {
        if (gotB) lockB.unlock(); // 释放锁B
        if (gotA) lockA.unlock(); // 释放锁A
    }
}).start();

原理:超时后主动放弃,避免 “持有锁 A 并永久等待锁 B”,打破 “持有并等待” 条件。

2. 支持中断(响应外部中断信号)

通过 lockInterruptibly() 允许等待锁的线程被中断(如其他线程调用 interrupt()),从而退出等待,避免死锁。

示例

ReentrantLock lock = new ReentrantLock();

Thread t1 = new Thread(() -> {
    try {
        // 可中断地获取锁:若线程被中断,会抛出InterruptedException
        lock.lockInterruptibly();
        try {
            // 执行逻辑(若长时间运行,外部可中断)
            Thread.sleep(10000);
        } finally {
            lock.unlock();
        }
    } catch (InterruptedException e) {
        System.out.println("线程1获取锁时被中断,退出");
    }
});
t1.start();

// 若t1长时间未释放锁,主线程可中断它
new Thread(() -> {
    try {
        Thread.sleep(1000);
        t1.interrupt(); // 中断t1,使其退出等待
    } catch (InterruptedException e) {}
}).start();

原理:线程可被外部中断,打破 “不可剥夺” 条件(无需等待锁释放,主动退出)。

3. 使用 tryLock () 的非阻塞尝试(避免等待)

通过 tryLock()(无参)尝试获取锁,若当前锁被占用则立即返回 false,可实现 “非阻塞” 逻辑,避免进入等待状态。

示例

if (lock.tryLock()) { // 若锁未被占用,立即获取
    try {
        // 执行逻辑
    } finally {
        lock.unlock();
    }
} else {
    // 锁被占用,执行备选逻辑(如重试、降级处理)
}

三、针对 synchronized 的注意事项(弥补其局限性)

synchronized 无超时、中断机制,需更依赖设计层面的规避:

1. 严格遵循锁的获取顺序(唯一可靠手段)

synchronized 无法主动放弃等待,固定锁的获取顺序是避免死锁的核心(如前文 “通用策略 1”)。

2. 避免在 synchronized 中调用外部方法

外部方法可能隐藏未知的锁操作(如调用其他类的同步方法),导致意外的锁嵌套和死锁。

反例

synchronized (lockA) {
    // 调用外部方法,可能内部持有lockB,导致锁顺序混乱
    externalService.doSomething(); 
}

优化: 提前确认外部方法是否涉及同步操作,或通过接口定义约束锁的使用。

3. 控制共享资源数量(减少互斥)

若多个线程竞争的资源(锁)数量越少,死锁概率越低。尽量合并或减少共享资源,降低锁的竞争面。

四、死锁检测与补救(事后处理)

若死锁已发生,可通过工具检测并恢复:

  • jstack 命令:打印线程栈,查看处于 BLOCKED 状态的线程及等待的锁,定位死锁。
  • JConsole/JVisualVM:可视化工具,可直接检测死锁并显示锁的持有关系。
  • 补救措施:重启服务(简单粗暴)或通过线程池的 shutdownNow() 中断所有线程(需线程支持中断)。

总结

避免死锁的核心思路是打破死锁的四个条件,具体措施因锁的类型而异:

  • 通用策略:固定锁的获取顺序、减少锁嵌套、缩短锁持有时间。
  • ReentrantLock:利用 tryLock(timeout) 超时机制、lockInterruptibly() 中断支持,主动避免永久等待。
  • synchronized:严格依赖锁的顺序控制,避免隐藏的锁嵌套。

实际开发中,预防优于补救,需在设计阶段就通过合理的锁策略规避死锁风险。

JAVA-技能点
知识点
多线程