源本科技 | 码上会

Java 中的死锁

2026/01/29
30
0

学习目标

  • 理解死锁在 Java 多线程中的定义与成因

  • 掌握死锁发生的典型场景及代码示例

  • 学会使用 JDK 工具检测运行中的死锁

  • 了解预防和减少死锁发生的常用策略


什么是死锁

在 Java 多线程编程中,死锁(Deadlock) 是指两个或多个线程因相互等待对方持有的锁而永久阻塞,导致程序无法继续执行的状态。

具体表现为:

  • 每个线程都持有一个锁,并试图获取另一个被其他线程持有的锁;

  • 形成循环等待,所有相关线程都无法继续推进;

  • 程序“冻结”,但并未崩溃,只是陷入无限等待。


死锁示例代码

以下是一个典型的 Java 死锁演示程序:

// 工具类:用于暂停线程
class Util {
    static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

// 被两个线程共享的类
class Shared {
    // 同步方法一
    synchronized void test1(Shared other) {
        System.out.println(Thread.currentThread().getName() 
                           + " 进入 " + this + " 的 test1 方法");
        Util.sleep(1000);
        
        // 尝试调用另一个对象的 test2 方法
        other.test2();
        System.out.println(Thread.currentThread().getName() 
                           + " 退出 " + this + " 的 test1 方法");
    }

    // 同步方法二
    synchronized void test2() {
        System.out.println(Thread.currentThread().getName() 
                           + " 进入 " + this + " 的 test2 方法");
        Util.sleep(1000);
        System.out.println(Thread.currentThread().getName() 
                           + " 退出 " + this + " 的 test2 方法");
    }
}

class WorkerThread1 extends Thread {
    private Shared s1, s2;
    public WorkerThread1(Shared s1, Shared s2) {
        this.s1 = s1;
        this.s2 = s2;
    }
    @Override
    public void run() {
        s1.test1(s2);
    }
}

class WorkerThread2 extends Thread {
    private Shared s1, s2;
    public WorkerThread2(Shared s1, Shared s2) {
        this.s1 = s1;
        this.s2 = s2;
    }
    @Override
    public void run() {
        s2.test1(s1);
    }
}

public class DeadlockDemo {
    public static void main(String[] args) {
        Shared s1 = new Shared();
        Shared s2 = new Shared();

        WorkerThread1 t1 = new WorkerThread1(s1, s2);
        t1.setName("线程 A");
        t1.start();

        WorkerThread2 t2 = new WorkerThread2(s1, s2);
        t2.setName("线程 B");
        t2.start();

        Util.sleep(2000); // 等待足够时间观察死锁
    }
}

执行流程说明

  1. 线程 A 获取 s1 对象的锁,进入 s1.test1(s2)

  2. 线程 B 获取 s2 对象的锁,进入 s2.test1(s1)

  3. test1 中,线程 A 尝试调用 s2.test2(),但 s2 的锁已被线程 B 持有;

  4. 同时,线程 B 尝试调用 s1.test2(),但 s1 的锁已被线程 A 持有;

  5. 两者互相等待,形成死锁,程序永久挂起。

注意:该程序不应在在线 IDE 中运行,因为它会导致进程卡死。建议在本地环境测试。


Java 中的锁机制

Java 使用内置锁,也称为监视器锁,通过 synchronized 关键字实现。每个对象都有一个与之关联的锁。

当一个线程进入 synchronized 方法或代码块时,它必须先获得该对象的锁;执行完毕后自动释放锁。

上图展示了典型的死锁环路:T1 持有 s1 并等待 s2,T2 持有 s2 并等待 s1。


如何检测死锁

Java 提供了强大的命令行工具来检测运行中的死锁:

1:列出 Java 进程

jps -l

输出示例:

12345 DeadlockDemo
67890 SomeOtherApp

2:打印线程堆栈并检查死锁

jcmd 12345 Thread.print

(将 12345 替换为实际的进程 ID)

在输出中,若存在死锁,JVM 会明确提示:

Found one Java-level deadlock:
=============================
"线程 A":
  waiting to lock monitor 0x00007f... (object s2)
  which is held by "线程 B"
"线程 B":
  waiting to lock monitor 0x00007f... (object s1)
  which is held by "线程 A"

此外,也可以使用 jstack <PID> 命令生成线程转储(Thread Dump)进行分析。


如何预防死锁

虽然无法完全消除死锁的可能性,但可通过以下策略显著降低其发生概率:

1. 避免嵌套锁

  • 不要在已经持有某个锁的情况下再去请求另一个锁;

  • 如果必须获取多个锁,确保所有线程以相同的顺序获取锁。

推荐做法

// 所有线程先获取 s1,再获取 s2
synchronized (s1) {
    synchronized (s2) {
        // 业务逻辑
    }
}

2. 只锁定必要资源

  • 仅对真正需要同步的代码段加锁;

  • 避免在同步块中执行耗时操作(如 I/O、sleep)。

3. 使用超时机制

  • 利用 tryLock(timeout)(来自 java.util.concurrent.locks.ReentrantLock)代替无限等待;

  • 若在指定时间内无法获得锁,则放弃并重试或回退。

4. 谨慎使用 Thread.join()

  • 当一个线程需等待另一个线程结束时,可使用带超时的 join(long millis)

    thread.join(2000); // 最多等待 2 秒
  • 避免无限制等待,防止因被等待线程阻塞而导致连锁死锁。


重点总结

  • 死锁是多线程中因循环等待锁而造成的永久阻塞;

  • 典型死锁需满足四个条件:互斥、持有并等待、不可抢占、循环等待;

  • Java 内置的 synchronized 机制若使用不当极易引发死锁;

  • 可通过 jcmdjstack 工具检测运行时死锁;

  • 预防死锁的关键在于统一加锁顺序减少锁粒度引入超时机制


思考题

  1. 在上述死锁示例中,如果将 test1test2 方法改为静态同步方法,是否还会发生死锁?为什么?

  2. 除了调整加锁顺序,你还能想到哪些设计模式或并发工具(如 ReentrantLockSemaphore)可用于避免死锁?

  3. 在实际项目中,如何通过代码审查或静态分析工具提前发现潜在的死锁风险?