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 等工具能帮助我们在问题发生后快速定位。

相关推荐
好家伙VCC8 小时前
**神经编码新视角:用Python实现生物启发的神经信号压缩与解码算法**在人工智能飞速发展的今天
java·人工智能·python·算法
一灯架构14 小时前
90%的人答错!一文带你彻底搞懂ArrayList
java·后端
踏着七彩祥云的小丑14 小时前
pytest——Mark标记
开发语言·python·pytest
Dream of maid15 小时前
Python12(网络编程)
开发语言·网络·php
W230357657315 小时前
经典算法:最长上升子序列(LIS)深度解析 C++ 实现
开发语言·c++·算法
Y40900115 小时前
【多线程】线程安全(1)
java·开发语言·jvm
不爱吃炸鸡柳15 小时前
Python入门第一课:零基础认识Python + 环境搭建 + 基础语法精讲
开发语言·python
布局呆星16 小时前
SpringBoot 基础入门
java·spring boot·spring
minji...16 小时前
Linux 线程同步与互斥(三) 生产者消费者模型,基于阻塞队列的生产者消费者模型的代码实现
linux·运维·服务器·开发语言·网络·c++·算法
Dxy123931021616 小时前
Python基于BERT的上下文纠错详解
开发语言·python·bert