java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java死锁原因及预防

Java死锁原因及预防方法超详细讲解

作者:走过冬季

Java死锁是多线程因相互等待资源而阻塞的问题,需满足互斥、持有并等待、不可剥夺、循环等待四个条件,这篇文章主要介绍了Java死锁原因及预防方法的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下

前言

Java 死锁是多线程编程中一种经典且棘手的问题,它会导致多个线程相互等待对方持有的资源而永久阻塞。理解其产生原因和预防措施至关重要。

一、 Java 死锁是如何产生的?

死锁的发生需要同时满足以下四个必要条件(缺一不可):

  1. 互斥使用 (Mutual Exclusion):

    • 资源(如对象锁、数据库连接、文件句柄等)一次只能被一个线程独占使用。
    • synchronized 关键字或 Lock 对象实现的锁机制本质上就提供了这种互斥性。
  2. 持有并等待 (Hold and Wait / Partial Allocation):

    • 一个线程在持有至少一个资源(锁)的同时,又去申请获取另一个线程当前正持有的资源(锁)。
  3. 不可剥夺 (No Preemption):

    • 一个线程已经获得的资源(锁)在它主动释放之前,不能被其他线程强行剥夺。
    • 在 Java 中,synchronized 锁不能被强制中断释放;Lock.lock() 获取的锁也不能被其他线程强制解锁(除非使用 Lock.lockInterruptibly() 并中断线程,但这通常也不是“强行剥夺”的含义)。
  4. 循环等待 (Circular Wait):

    • 存在一组等待的线程 {T1, T2, ..., Tn},其中:
      • T1 等待 T2 持有的资源,
      • T2 等待 T3 持有的资源,
      • …,
      • Tn 等待 T1 持有的资源。
    • 所有线程形成一个等待资源的环。

经典死锁场景示例(哲学家就餐问题简化版)

public class DeadlockExample {

    static final Object lockA = new Object();
    static final Object lockB = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (lockA) { // 线程1获取lockA
                System.out.println("Thread1 acquired lockA");
                try {
                    Thread.sleep(100); // 模拟操作,增加死锁发生概率
                } catch (InterruptedException e) {}
                synchronized (lockB) { // 线程1尝试获取lockB(此时可能被线程2持有)
                    System.out.println("Thread1 acquired lockB");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (lockB) { // 线程2获取lockB
                System.out.println("Thread2 acquired lockB");
                try {
                    Thread.sleep(100); // 模拟操作,增加死锁发生概率
                } catch (InterruptedException e) {}
                synchronized (lockA) { // 线程2尝试获取lockA(此时被线程1持有)
                    System.out.println("Thread2 acquired lockA");
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}

分析死锁条件满足情况

  1. 互斥: lockAlockB 都是 synchronized 使用的对象,具有互斥性。
  2. 持有并等待:
    • 线程1 持有 lockA,同时等待获取 lockB
    • 线程2 持有 lockB,同时等待获取 lockA
  3. 不可剥夺: Java synchronized 锁不能被其他线程强行剥夺。
  4. 循环等待:
    • 线程1 在等待线程2 释放的 lockB
    • 线程2 在等待线程1 释放的 lockA
    • 形成了一个闭环:线程1 -> 等待lockB(被线程2持有) -> 线程2 -> 等待lockA(被线程1持有) -> 线程1。

二、 如何防止 Java 死锁?

防止死锁的核心策略就是破坏上述四个必要条件中的至少一个。以下是常用的方法:

1. 破坏"循环等待"条件 - 锁顺序化 (Lock Ordering)

Thread thread1 = new Thread(() -> {
    synchronized (lockA) {
        System.out.println("Thread1 acquired lockA");
        try { Thread.sleep(100); } catch (InterruptedException e) {}
        synchronized (lockB) { // 总是先A后B
            System.out.println("Thread1 acquired lockB");
        }
    }
});

Thread thread2 = new Thread(() -> {
    synchronized (lockA) { // 线程2也先尝试获取lockA
        System.out.println("Thread2 acquired lockA");
        try { Thread.sleep(100); } catch (InterruptedException e) {}
        synchronized (lockB) { // 再获取lockB
            System.out.println("Thread2 acquired lockB");
        }
    }
});

2. 破坏"持有并等待"条件 - 一次性申请所有锁 (Atomically Acquire All Locks)

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class DeadlockPrevention {

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

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> acquireLocksAndWork(lockA, lockB, "Thread1"));
        Thread thread2 = new Thread(() -> acquireLocksAndWork(lockB, lockA, "Thread2")); // 注意顺序不同,但方法内部处理
        thread1.start();
        thread2.start();
    }

    public static void acquireLocksAndWork(Lock firstLock, Lock secondLock, String threadName) {
        while (true) {
            boolean gotFirst = false;
            boolean gotSecond = false;
            try {
                // 尝试获取第一个锁(带超时避免无限等待)
                gotFirst = firstLock.tryLock(100, TimeUnit.MILLISECONDS);
                if (gotFirst) {
                    System.out.println(threadName + " acquired first lock");
                    // 尝试获取第二个锁(带超时)
                    gotSecond = secondLock.tryLock(100, TimeUnit.MILLISECONDS);
                    if (gotSecond) {
                        System.out.println(threadName + " acquired second lock");
                        // 成功获取两个锁,执行工作
                        System.out.println(threadName + " doing work...");
                        Thread.sleep(500); // 模拟工作
                        break; // 工作完成,跳出循环
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // 无论如何,在退出前确保释放已获得的锁
                if (gotSecond) secondLock.unlock();
                if (gotFirst) firstLock.unlock();
            }
            // 如果没能一次性获得两个锁,等待随机时间后重试,避免活锁
            try {
                Thread.sleep((long) (Math.random() * 100));
            } catch (InterruptedException e) {}
        }
    }
}

3. 避免不必要的锁 / 缩小锁的范围

4. 使用锁超时 (Lock Timeout) - 破坏"不可剥夺"的间接效果

5. 死锁检测与恢复

总结与建议

  1. 首选锁顺序化: 在设计多锁交互时,强制全局一致的锁获取顺序是最有效且推荐的预防策略。
  2. 善用 Lock 和 tryLock: 当锁顺序难以严格保证或需要更灵活控制时,使用 ReentrantLock 及其 tryLock(带超时)方法,实现一次性申请所有锁或锁超时机制。务必在 finally 块中释放锁
  3. 良好的并发习惯:
    • 最小化锁范围(缩小 synchronized 块)。
    • 优先使用并发集合 (java.util.concurrent.*) 和原子变量。
    • 考虑不可变对象和线程本地存储 (ThreadLocal)。
  4. 避免嵌套锁: 尽量避免在一个锁保护的代码块内再去获取另一个锁。如果必须,严格应用锁顺序化。
  5. 超时机制: 在可能长时间等待的地方(包括锁获取、条件等待 Condition.await、线程 joinFuture.get 等)使用超时参数,防止永久阻塞,给系统提供回退的机会。
  6. 工具检测: 利用 JConsole、VisualVM、jstack 命令行工具等定期检查或在线诊断潜在的死锁。jstack -l <pid> 输出的线程转储会明确标识出找到的死锁和涉及的线程/锁。

记住: 预防死锁的关键在于设计和编码阶段就意识到风险并应用上述策略。事后检测和恢复往往是代价高昂的最后手段。💡

到此这篇关于Java死锁原因及预防方法的文章就介绍到这了,更多相关Java死锁原因及预防内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文