浅析死锁及解决方案

101 阅读9分钟

将从死锁产生的核心原理(结合 Android 场景)、具体产生过程、解决方案(Android 实践导向)  三个维度展开,并通过时序图直观呈现死锁形成与解决的完整流程。

一、线程死锁的核心定义与 Android 场景特性

线程死锁是指多个线程(如子线程、主线程)互相持有对方必需的锁资源,且均不释放已持有的锁,导致所有线程永久阻塞的状态。在 Android 中,死锁的危害尤为明显:

  • 主线程(UI 线程)  参与死锁,会直接触发 ANR(Application Not Responding);
  • 子线程(如网络线程、数据库线程)  死锁,会导致功能卡死(如数据同步失败、任务无法执行)。

Android 中常见的锁类型(也是死锁的核心载体)包括:

  • 隐式锁:synchronized(修饰方法、代码块,依赖 JVM 底层实现);
  • 显式锁:java.util.concurrent.locks包下的ReentrantLockReadWriteLock(更灵活,支持中断、超时);
  • 其他锁:Object.wait()/notify()的对象监视器锁、Room 数据库的事务锁等。

二、线程死锁的产生条件(四大必要条件)

死锁的产生必须同时满足以下 4 个条件,缺一不可。结合 Android 实际场景拆解如下:

必要条件核心含义Android 场景示例
1. 互斥条件锁资源只能被一个线程持有,同一时间无法被多个线程共享synchronized修饰的 “用户信息单例” 的updateUser()方法,同一时间只有一个线程能执行;Room 数据库的某张表锁,同一时间只能有一个写操作线程持有。
2. 持有并等待条件线程持有一个锁后,不释放该锁,同时等待另一个锁线程 A 持有 “用户数据锁”,还想获取 “订单数据锁” 以完成 “同步用户与订单” 的任务,此时会等待 “订单锁”。
3. 不可剥夺条件线程持有的锁不能被强制夺走,只能由线程主动释放synchronized锁一旦被线程持有,只有线程执行完同步代码块或发生异常时才会释放,其他线程无法强制抢占;ReentrantLock若未调用unlock(),也不会被强制释放。
4. 循环等待条件多个线程形成 “等待闭环”:线程 A 等线程 B 的锁,线程 B 等线程 A 的锁线程 A 持有 “用户锁” 等 “订单锁”,线程 B 持有 “订单锁” 等 “用户锁”,形成循环等待。

三、Android 死锁产生的具体过程(案例 + 时序图)

“用户数据与订单数据同步” 这个典型 Android 场景为例,拆解死锁的完整产生过程。

1. 场景前提

  • 存在两个锁资源:userLock(用户数据锁)、orderLock(订单数据锁);

  • 两个同步方法:

    • updateUser():需持有userLock,更新用户信息;
    • updateOrder():需持有orderLock,更新订单信息;
  • 两个业务方法(存在锁嵌套):

    • syncUserThenOrder():先调用updateUser()(获userLock),再调用updateOrder()(等orderLock);
    • syncOrderThenUser():先调用updateOrder()(获orderLock),再调用updateUser()(等userLock);
  • 两个线程:

    • 线程 1(子线程):执行syncUserThenOrder()
    • 线程 2(子线程):执行syncOrderThenUser()

2. 死锁产生时序图(Mermaid 语法,可直接渲染)

image.png

orderLock(订单锁)userLock(用户锁)线程2(执行syncOrderThenUser)线程1(执行syncUserThenOrder)orderLock(订单锁)userLock(用户锁)线程2(执行syncOrderThenUser)线程1(执行syncUserThenOrder)初始状态:所有锁均未被持有此时T1持有UL,T2持有OL循环等待形成(T1等OL,T2等UL),不可剥夺条件满足→死锁产生申请获取userLock(执行updateUser())获取成功(互斥条件满足)申请获取orderLock(执行updateOrder())获取成功(互斥条件满足)申请获取orderLock(执行updateOrder())被T2持有,进入等待(持有并等待条件满足)申请获取userLock(执行updateUser())被T1持有,进入等待(持有并等待条件满足)

3. 死锁产生步骤拆解

  1. 初始竞争:线程 1 和线程 2 几乎同时启动,分别申请userLockorderLock,因两个锁均未被持有,均获取成功(满足 “互斥条件”);
  2. 持有并等待:线程 1 持有userLock后,继续执行updateOrder(),需申请orderLock,但orderLock已被线程 2 持有,线程 1 进入BLOCKED状态(满足 “持有并等待条件”);
  3. 循环等待:线程 2 持有orderLock后,继续执行updateUser(),需申请userLock,但userLock已被线程 1 持有,线程 2 也进入BLOCKED状态;
  4. 死锁固化:线程 1 等待线程 2 的orderLock,线程 2 等待线程 1 的userLock,形成闭环(满足 “循环等待条件”);且两者的锁无法被强制剥夺(满足 “不可剥夺条件”),最终永久阻塞。

四、Android 并发死锁的解决方案(实践导向)

解决死锁的核心思路是破坏四大必要条件中的任意一个或多个,结合 Android 开发场景,给出可落地的方案:

方案 1:破坏 “循环等待” 条件 —— 统一锁的获取顺序(最常用)

核心逻辑

规定所有线程必须按固定顺序获取锁(如按锁对象的哈希值排序、按业务优先级排序),避免循环等待。

Android 实践示例

在上述案例中,强制所有线程必须先获取userLock,再获取orderLock,即使是syncOrderThenUser()方法,也需调整顺序:

// 锁顺序规则:先userLock,后orderLock
private final Object userLock = new Object();
private final Object orderLock = new Object();

// 业务方法1:同步用户→订单(符合顺序)
public void syncUserThenOrder() {
    synchronized (userLock) { // 先拿userLock
        updateUser();
        synchronized (orderLock) { // 再拿orderLock
            updateOrder();
        }
    }
}

// 业务方法2:同步订单→用户(调整为符合顺序)
public void syncOrderThenUser() {
    synchronized (userLock) { // 强制先拿userLock(不再先拿orderLock)
        updateUser();
        synchronized (orderLock) {
            updateOrder();
        }
    }
}

解决后时序图

image.png

orderLockuserLock线程2(syncOrderThenUser)线程1(syncUserThenOrder)orderLockuserLock线程2(syncOrderThenUser)线程1(syncUserThenOrder)T1持有UL,继续执行T1释放UL后,T2继续申请userLock获取成功申请userLock(按顺序)被T1持有,进入等待申请orderLock获取成功执行updateUser() + updateOrder()释放orderLock释放userLock获取成功申请orderLock获取成功执行updateUser() + updateOrder()释放orderLock释放userLock

方案 2:破坏 “持有并等待” 条件 —— 一次性获取所有锁

核心逻辑

线程在执行任务前,一次性申请所有必需的锁,若有任意一个锁无法获取,则释放已申请的锁并等待,避免 “持有部分锁等待其他锁”。

Android 实践示例(用ReentrantLocktryLock()实现)

private final Lock userLock = new ReentrantLock();
private final Lock orderLock = new ReentrantLock();

public void syncData() {
    boolean gotUserLock = false;
    boolean gotOrderLock = false;

    try {
        // 一次性申请所有锁(超时100ms,避免永久等待)
        gotUserLock = userLock.tryLock(100, TimeUnit.MILLISECONDS);
        if (gotUserLock) {
            gotOrderLock = orderLock.tryLock(100, TimeUnit.MILLISECONDS);
        }

        // 只有所有锁都获取成功,才执行业务逻辑
        if (gotUserLock && gotOrderLock) {
            updateUser();
            updateOrder();
        } else {
            // 部分锁获取失败,重试或降级处理
            Log.w("DeadLock", "部分锁获取失败,重试或降级");
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt(); // 恢复中断状态
    } finally {
        // 释放已获取的锁(避免泄漏)
        if (gotOrderLock) orderLock.unlock();
        if (gotUserLock) userLock.unlock();
    }
}

方案 3:破坏 “不可剥夺” 条件 —— 使用可中断锁

核心逻辑

使用支持中断的锁(如ReentrantLocklockInterruptibly()),当线程等待锁超时或被标记为中断时,主动放弃等待并释放已持有的锁,打破 “不可剥夺”。

Android 实践示例(主线程中断子线程避免死锁)

// 子线程:执行可能死锁的任务
private Thread syncThread = new Thread(() -> {
    try {
        userLock.lockInterruptibly(); // 可中断锁
        try {
            orderLock.lockInterruptibly();
            updateUser();
            updateOrder();
        } finally {
            orderLock.unlock();
        }
    } catch (InterruptedException e) {
        // 被中断,释放资源并退出
        Log.i("DeadLock", "线程被中断,避免死锁");
        Thread.currentThread().interrupt();
    } finally {
        if (userLock.isHeldByCurrentThread()) {
            userLock.unlock();
        }
    }
});

// 主线程:检测到子线程可能死锁(如超时),中断子线程
public void checkAndInterrupt() {
    syncThread.start();
    // 超时检测(如5秒未完成则中断)
    new Handler(Looper.getMainLooper()).postDelayed(() -> {
        if (syncThread.isAlive()) {
            syncThread.interrupt(); // 中断子线程,释放锁
        }
    }, 5000);
}

方案 4:避免嵌套锁 + 使用无锁并发工具(从源头减少锁竞争)

核心逻辑

  • 减少锁的嵌套层级:将需多个锁的逻辑拆分为单一锁逻辑,或用更细粒度的锁(如ReadWriteLock,读操作共享,写操作互斥);
  • 用无锁工具替代显式锁:Android 中优先使用java.util.concurrent包下的线程安全容器或原子类,避免手动加锁。

Android 实践示例

  1. ReadWriteLock优化读写锁竞争

    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock(); // 读锁共享
    private final Lock writeLock = rwLock.writeLock(); // 写锁互斥
    
    // 读操作(多线程可同时执行)
    public User getUser() {
        readLock.lock();
        try {
            return userDao.queryUser();
        } finally {
            readLock.unlock();
        }
    }
    
    // 写操作(单线程执行)
    public void updateUser(User user) {
        writeLock.lock();
        try {
            userDao.updateUser(user);
        } finally {
            writeLock.unlock();
        }
    }
    
  2. 用原子类替代synchronized:若仅需原子更新变量(如计数器、状态标记),用AtomicIntegerAtomicBoolean等原子类(基于 CAS 操作,无锁):

    // 原子计数器(无需synchronized)
    private final AtomicInteger syncCount = new AtomicInteger(0);
    
    public void incrementSyncCount() {
        syncCount.incrementAndGet(); // 原子操作,线程安全
    }
    

方案 5:死锁检测与恢复(Android 工具链支持)

当死锁已发生时,需通过工具检测并手动 / 自动恢复:

  1. 检测工具

    • Android Studio Profiler:打开 “App Inspection”→“Threads”,死锁线程会被标记为BLOCKED,并显示 “Waiting for lock” 的具体信息;
    • jstack 命令:通过adb shell ps | grep 包名获取进程 PID,再执行jstack PID,日志中会明确标注 “DEADLOCK” 及锁持有关系。
  2. 恢复策略

    • 中断死锁线程:通过Thread.interrupt()中断其中一个线程,释放锁;
    • 重启任务:检测到死锁后,终止当前任务并重新启动(如重新发起数据同步请求);
    • 服务降级:若死锁影响核心功能,临时降级为本地缓存数据,保证 App 基础可用。

五、总结

Android 线程死锁的本质是锁资源的不合理竞争与等待关系,解决的核心是:

  1. 预防优先:通过 “统一锁顺序”“一次性获取锁”“避免嵌套锁” 等方案,从源头破坏死锁的必要条件;
  2. 工具辅助:用 Profiler、jstack 等工具及时检测死锁,用ReentrantLock、原子类等更灵活的并发工具替代传统锁;
  3. 容错兜底:设置超时、中断机制,避免死锁导致 App 完全不可用。

在实际 Android 开发中,需结合业务场景(如 UI 线程、数据库、网络请求)选择合适的方案,优先保证主线程不阻塞、核心功能不卡死。