Java并发——死锁

在多线程编程中,死锁(Deadlock)是一个经典而又棘手的问题。它如同程序的"交通堵塞",让多个线程互相等待对方释放资源,最终谁也无法继续执行,导致系统挂起。理解死锁的原理、学会避免和排查死锁,是每一位并发程序员的必修课。

本文将从死锁的定义开始,通过生动的代码示例演示死锁的产生,剖析其产生的四个必要条件,并给出多种预防和检测方法,助你远离死锁的困扰。

一、什么是死锁?

死锁是指两个或两个以上的线程,在执行过程中因争夺资源而造成的一种互相等待的现象。若无外力干预,它们都将无法推进下去。简单来说,就是:

线程 A 持有资源 1,等待资源 2;线程 B 持有资源 2,等待资源 1。双方都得不到想要的资源,于是永远阻塞。

二、死锁的四个必要条件

死锁的发生必须同时满足以下四个条件,缺一不可:

  1. 互斥条件:资源一次只能被一个线程占用,如果其他线程请求该资源,只能等待直到释放。

  2. 持有并等待条件:线程已经持有了至少一个资源,又在等待其他资源,且不释放已持有的资源。

  3. 不可剥夺条件:线程已获得的资源,在未使用完之前,不能被其他线程强行剥夺,只能由自己主动释放。

  4. 循环等待条件:存在一组线程的循环等待链,即 T0 等待 T1 持有的资源,T1 等待 T2 持有的资源,......,Tn 等待 T0 持有的资源。

只要破坏其中任何一个条件,死锁就不会发生。

三、死锁代码示例

下面是一个经典的死锁示例,两个线程分别持有一个锁,并试图获取对方的锁:

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

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("线程1 获得 lock1,等待 lock2...");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                synchronized (lock2) {
                    System.out.println("线程1 同时持有 lock1 和 lock2");
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("线程2 获得 lock2,等待 lock1...");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                synchronized (lock1) {
                    System.out.println("线程2 同时持有 lock2 和 lock1");
                }
            }
        });

        t1.start();
        t2.start();
    }
}

运行结果:

java 复制代码
线程1 获得 lock1,等待 lock2...
线程2 获得 lock2,等待 lock1...

之后程序便停滞不前,两个线程互相等待,形成死锁。

四、如何避免死锁?

根据死锁的四个必要条件,我们可以采取相应的预防策略:

1. 破坏"持有并等待"

方法 :一次性申请所有资源,如果申请失败则释放已持有的所有资源。在 Java 中,可以使用 ReentrantLocktryLock 方法尝试获取锁,超时则放弃。

java 复制代码
import java.util.concurrent.locks.ReentrantLock;

public class AvoidDeadlock {
    private static final ReentrantLock lock1 = new ReentrantLock();
    private static final ReentrantLock lock2 = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (true) {
                if (lock1.tryLock()) {
                    try {
                        if (lock2.tryLock()) {
                            try {
                                System.out.println("线程1 成功获取两把锁");
                                break;
                            } finally {
                                lock2.unlock();
                            }
                        }
                    } finally {
                        lock1.unlock();
                    }
                }
                // 短暂等待后重试
                try { Thread.sleep(10); } catch (InterruptedException e) {}
            }
        });
        // 线程2 类似
        // ...
    }
}

2. 破坏"不可剥夺"

如果线程在申请新资源时失败,可以主动释放已持有的资源。上面 tryLock 的方式实际上也体现了这一思想。

3. 破坏"循环等待"

最常用的方法:对所有资源进行统一排序,规定线程必须按顺序申请资源。例如,总是先获取 lock1,再获取 lock2,这样就不会出现循环等待。

java 复制代码
synchronized (lock1) {
    synchronized (lock2) {
        // 安全
    }
}

4. 使用更高层次的并发工具

例如使用 java.util.concurrent 包中的 SemaphoreCountDownLatchBlockingQueue 等,它们内部已经处理好资源竞争,减少了手写锁导致死锁的机会。

五、如何检测死锁?

即使代码编写时注意了预防,死锁仍可能因复杂逻辑而偶然出现。我们需要学会在运行时检测死锁。

1. jstack 命令

使用 jstack <pid> 可以打印出 Java 进程的线程堆栈,其中会明确提示死锁信息:

复制代码
Found one Java-level deadlock:
=============================
"线程2":
  waiting to lock monitor 0x... (object 0x..., a java.lang.Object),
  which is held by "线程1"
"线程1":
  waiting to lock monitor 0x... (object 0x..., a java.lang.Object),
  which is held by "线程2"

2. JConsole / VisualVM

这些图形化工具可以直接连接到 JVM,在"线程"选项卡中点击"检测死锁"按钮,即可快速定位。

3. 编程方式检测

通过 ThreadMXBean 可以在程序中主动检测死锁:

复制代码
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
if (deadlockedThreads != null) {
    // 有死锁发生,可以记录日志或尝试恢复
}

六、死锁恢复策略

一旦发生死锁,通常的恢复手段有:

  • 重启应用(最直接,但可能导致数据丢失)。

  • 杀死其中一个线程(强行释放资源),但 Java 并没有提供直接杀死线程的安全方法,通常通过中断来协作。

  • 使用超时机制 :如 tryLock(long, TimeUnit) 在等待一定时间后放弃,然后释放已持有锁,让其他线程有机会执行。

七、总结

死锁是多线程编程中难以完全避免的问题,但通过理解其产生的四个必要条件,我们可以有针对性地采用预防策略:按顺序申请资源、使用超时锁、减少锁持有时间等。同时,掌握 jstack、JConsole 等工具能帮助我们在问题发生后快速定位。

相关推荐
我是人✓1 小时前
从零入门 Servlet:JavaWeb 核心组件的实操与理解
java·servlet
lay_liu1 小时前
Spring Boot 自动配置
java·spring boot·后端
qq_283720051 小时前
Qt QML 中为 CheckBox 设置鸿蒙字体(HarmonyOS Sans)——适配 Qt 5.6.x 与 Qt 5.12+
开发语言·qt·harmonyos
未知鱼1 小时前
Python安全开发之简易目录扫描器(含详细注释)
开发语言·python·安全
殷紫川2 小时前
线上故障零扩散:全链路监控、智能告警与应急响应 SOP 完整落地指南
java·架构·监控
前端小雪的博客.2 小时前
Java的面向对象:封装详解(0基础入门版)
java·java入门·java面向对象·封装详解·java封装·0基础学java·getter和setter
小白橘颂2 小时前
【C语言】基础概念梳理(一)
c语言·开发语言·stm32·单片机·mcu·物联网·51单片机
沫离痕2 小时前
AI机器人客服-Dify接入
开发语言·javascript·ecmascript
ShayneLee82 小时前
jar-替换依赖包
java·jar