如何避免死锁?

访客 性能优化 2

如何避免死锁?一文掌握死锁预防与解决方案

目录导读

  1. 什么是死锁?——核心概念与经典案例
  2. 死锁产生的四个必要条件(互斥、持有并等待、非剥夺、循环等待)
  3. 如何避免死锁?六大实战策略
    • 破坏互斥条件
    • 破坏持有并等待条件
    • 破坏非剥夺条件
    • 破坏循环等待条件
    • 银行家算法(系统级预防)
    • 超时与重试机制(工程实践)
  4. 常见问题问答(Q&A)
  5. 总结与最佳实践建议

什么是死锁?

死锁是指两个或多个线程(或进程)在争夺资源时,彼此都在等待对方释放资源,从而陷入无限等待的状态,线程A持有资源1,等待资源2;线程B持有资源2,等待资源1,两者都不愿放手,程序卡死。

经典案例:数据库中的事务死锁、操作系统的进程调度、多线程并发编程中的锁竞争。


死锁产生的四个必要条件

根据计算机科学家Coffman的总结,只要同时满足以下四个条件,死锁就必然发生:

条件 说明 例子
互斥 资源一次只能被一个线程使用 打印机、文件锁
持有并等待 线程持有资源的同时等待其他资源 线程A持有锁1,等待锁2
非剥夺 资源不能被强制拿走,只能主动释放 操作系统无法强制回收用户锁
循环等待 线程之间形成资源等待的环形链 A→B→C→A

核心原则:只要破坏上述任一条件,死锁就不会发生。


如何避免死锁?六大实战策略

破坏互斥条件

尽量使用共享资源代替互斥资源,使用读写锁(读操作可并发,写操作互斥);或使用无锁编程(Atomic操作、CAS算法)代替互斥锁。

适用场景:读多写少的系统(如缓存、配置中心)。

// 示例:使用读写锁避免不必要的互斥
ReadWriteLock rwLock = new ReentrantReadWriteLock();
rwLock.readLock().lock(); // 多个读线程可同时访问

破坏持有并等待条件

让线程在开始执行前一次性申请所有需要的资源,若资源不足,则让线程等待并释放已持有的资源。

实现方法

  • 在任务开始前,通过一个ResourceManager统一分配资源
  • 使用预分配模式(如JDBC连接池一次性获得所有连接)
public void runTask() {
    synchronized (lock1) {
        synchronized (lock2) { // 同时申请两个锁
            // 执行任务
        }
    }
}

风险:可能导致资源利用率降低(占用资源却未使用)。

破坏非剥夺条件

允许资源被强制剥夺,常见做法:

  • 使用可中断的锁(如Java的ReentrantLock.lockInterruptibly()
  • 引入超时机制:线程在规定时间内获取不到所有资源,就主动释放已持有资源,并重试。
ReentrantLock lock = new ReentrantLock();
if (lock.tryLock(100, TimeUnit.MILLISECONDS)) { 
    // 获取成功
} else {
    // 超时后释放已有资源,重新尝试
}

破坏循环等待条件

对所有资源进行全局唯一编号,要求线程必须按编号顺序(递增或递减)申请资源,这被称为资源有序分配法

示例:假设资源编号1,2,3,所有线程都必须先申请小号资源,再申请大号资源,这样就不会出现循环等待。

# 伪代码:按资源ID顺序加锁
def task():
    with lock1:  # 先小号
        with lock2:  # 后大号
            # 执行

优点:实现简单、效率高。缺点:编号难以在动态系统中维护。

银行家算法(系统级预防)

操作系统级算法,系统维护一个安全状态表,只有分配资源后系统仍处于安全状态,才分配资源,算法复杂度较高,适合单机多进程调度。

核心公式Available[j] + Need[i][j] > Request[i][j] 才能满足请求。

超时与重试机制(工程实践)

在分布式系统中,使用乐观锁+重试避免死锁,数据库更新时使用version字段,若更新失败则重试(配合退避策略)。

UPDATE account SET balance = balance - 100, version = version+1 
WHERE id = 1 AND version = 10;  -- 假设当前version=10
-- 若更新影响行数为0,说明版本冲突,重试

常见问题问答(Q&A)

Q1:死锁检测和死锁避免有什么区别?

A:死锁检测是事后处理(允许死锁发生,然后通过工具检测并恢复);死锁避免是事前预防(通过算法或策略组织死锁发生),生产环境中更推荐避免而非检测。

Q2:为什么动态资源分配(如Java synchronized)更容易死锁?

A:因为synchronized不支持中断和超时,一旦进入等待,只能死等,而ReentrantLock提供了tryLock()lockInterruptibly()等机制,更容易破坏非剥夺条件。

Q3:分布式系统中如何避免死锁?

A:常用方法包括:分布式锁超时(Redis锁设置过期时间)、有序加锁(按业务ID哈希排序)、TCC(Try-Confirm-Cancel)模式(通过补偿事务打破僵局)。

Q4:避免死锁的终极原则是什么?

A不要同时持有多个锁,如果必须持有多个锁,务必按固定顺序获取,并设置超时后主动释放。


总结与最佳实践建议

推荐策略组合(按应用场景)

场景 推荐方案
简单单机多线程 资源有序分配法(破坏循环等待)
高并发低延迟 无锁编程 + CAS(破坏互斥条件)
数据库事务 超时重试 + 版本号乐观锁
复杂资源依赖 银行家算法或预分配模式

必应/谷歌SEO优化关键词

  • 死锁避免方法
  • 死锁预防策略
  • 多线程死锁解决方案
  • 数据库死锁处理
  • Java死锁避免
  • 分布式系统死锁

最终建议

最有效的避免死锁方法,就是减少锁的使用,在设计中优先考虑:

  1. 尽量使用不可变数据结构(Immutable)
  2. 利用ThreadLocal、Actor模型等避免共享
  3. 使用消息队列代替锁(异步解耦)

如果你正在设计高并发系统,请牢记:锁是最后一根稻草,避免死锁的最佳方式就是避免加锁,通过合理的数据拆分和无锁算法,你可以构建一个既高效又安全的系统。

标签: 资源排序

抱歉,评论功能暂时关闭!