死锁的产生以及如何避免

死锁的产生与避免

一、死锁的产生原因

死锁是多个线程(或进程)因竞争资源而陷入无限等待的状态,需同时满足以下 四个必要条件

  1. 互斥条件(Mutual Exclusion)

    • 资源一次只能被一个线程独占使用(如锁、文件句柄)。
  2. 请求与保持(Hold and Wait)

    • 线程在持有至少一个资源的同时,请求其他线程占有的资源。
  3. 不可剥夺(No Preemption)

    • 资源只能由持有者主动释放,不能被强制抢占。
  4. 循环等待(Circular Wait)

    • 多个线程形成环形等待链,每个线程都在等待下一个线程释放资源。

二、典型死锁场景示例

Java 代码示例

java 复制代码
public class DeadlockDemo {
    private static final Object lockA = new Object();
    private static final Object lockB = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (lockA) {
                System.out.println("Thread1 holds lockA");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                synchronized (lockB) {  // 等待Thread2释放lockB
                    System.out.println("Thread1 holds lockB");
                }
            }
        }).start();

        new Thread(() -> {
            synchronized (lockB) {
                System.out.println("Thread2 holds lockB");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                synchronized (lockA) {  // 等待Thread1释放lockA
                    System.out.println("Thread2 holds lockA");
                }
            }
        }).start();
    }
}

结果

两个线程互相等待对方释放锁,程序无限卡死。


三、死锁的检测与诊断

1. 使用工具检测死锁

  • jstack (Java自带工具):

    bash 复制代码
    jstack <pid>  # 输出线程快照,显示死锁的线程及持有/等待的锁
  • VisualVMJConsole
    图形化界面查看线程状态,直接标记死锁。

2. 日志分析

若日志中线程长时间处于 BLOCKED 状态且无进展,可能发生死锁。


四、死锁的避免策略

1. 破坏"请求与保持"条件

  • 一次性申请所有资源
    线程在开始执行前申请全部所需资源,否则不执行。
    缺点:资源利用率低,可能导致饥饿。

2. 破坏"不可剥夺"条件

  • 允许抢占资源
    若线程请求资源失败,强制释放已持有的资源(需支持回滚操作)。
    缺点:实现复杂,适用于特定场景(如数据库事务)。

3. 破坏"循环等待"条件

  • 资源有序分配法

    为所有资源类型定义全局顺序,线程按顺序申请资源。
    示例

    规定必须先申请 lockA 再申请 lockB,避免交叉申请。

    java 复制代码
    // 修改后代码:两个线程均按 lockA → lockB 顺序申请
    new Thread(() -> {
        synchronized (lockA) {
            synchronized (lockB) { /* 逻辑 */ }
        }
    }).start();
    
    new Thread(() -> {
        synchronized (lockA) {
            synchronized (lockB) { /* 逻辑 */ }
        }
    }).start();

4. 使用超时机制

  • 尝试获取锁时设置超时
    若在指定时间内未获得锁,放弃并释放已持有的资源,避免无限等待。
    Java实现 (使用 ReentrantLock):

    java 复制代码
    Lock lockA = new ReentrantLock();
    Lock lockB = new ReentrantLock();
    
    if (lockA.tryLock(1, TimeUnit.SECONDS)) {
        try {
            if (lockB.tryLock(1, TimeUnit.SECONDS)) {
                try { /* 逻辑 */ } 
                finally { lockB.unlock(); }
            }
        } finally { lockA.unlock(); }
    }

5. 减少锁的粒度

  • 缩小同步范围
    仅对必要代码加锁,减少锁的持有时间。
  • 使用线程安全的数据结构
    ConcurrentHashMap 替代 synchronized + HashMap

五、最佳实践总结

策略 适用场景 优点 缺点
资源有序分配 多锁交叉申请场景 简单有效,预防循环等待 需全局统一顺序,可能限制灵活性
超时机制 高并发、允许重试的场景 避免无限等待,提升系统健壮性 需处理超时重试逻辑
无锁编程(CAS、原子类) 低竞争、简单操作场景 高性能,无死锁风险 复杂逻辑实现困难
事务回滚 数据库、支持回滚的操作 保证数据一致性 实现成本高

六、总结

死锁的避免需结合业务场景选择合适的策略:

  • 关键系统(如金融交易):优先使用资源有序分配和超时机制。
  • 高并发系统:减少锁粒度,采用无锁数据结构。
  • 复杂事务:结合事务管理和回滚机制。

通过代码规范、工具检测和设计优化,可显著降低死锁发生概率。

相关推荐
缺点内向11 小时前
Java:创建、读取或更新 Excel 文档
java·excel
带刺的坐椅11 小时前
Solon v3.4.7, v3.5.6, v3.6.1 发布(国产优秀应用开发框架)
java·spring·solon
四谎真好看13 小时前
Java 黑马程序员学习笔记(进阶篇18)
java·笔记·学习·学习笔记
桦说编程13 小时前
深入解析CompletableFuture源码实现(2)———双源输入
java·后端·源码
java_t_t13 小时前
ZIP工具类
java·zip
lang2015092813 小时前
Spring Boot优雅关闭全解析
java·spring boot·后端
pengzhuofan14 小时前
第10章 Maven
java·maven
百锦再15 小时前
Vue Scoped样式混淆问题详解与解决方案
java·前端·javascript·数据库·vue.js·学习·.net
刘一说15 小时前
Spring Boot 启动慢?启动过程深度解析与优化策略
java·spring boot·后端
壹佰大多15 小时前
【spring如何扫描一个路径下被注解修饰的类】
java·后端·spring