线程死锁

线程死锁:原理、成因与解决方案

线程死锁是多线程编程中常见的隐性问题,指两个或多个线程相互等待对方持有的资源,导致所有线程陷入无限期阻塞的状态。死锁一旦发生,程序往往无法自动恢复,需通过代码设计规避或手动干预解决。

死锁的核心成因

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

  1. 互斥条件
    资源具有排他性,同一时间只能被一个线程占用(如synchronized锁、文件句柄等)。
  2. 请求与保持条件
    线程持有部分资源,同时又请求其他线程已持有的资源,且不释放自身已占有的资源。
  3. 不剥夺条件
    线程已获得的资源,在未主动释放前,不能被其他线程强行剥夺(如 Java 的内置锁无法被强制释放)。
  4. 循环等待条件
    多个线程形成环形等待链,每个线程都在等待下一个线程持有的资源(如线程 A 等线程 B 的资源,线程 B 等线程 A 的资源)。

死锁示例代码

以下是一个典型的死锁场景:两个线程分别持有对方需要的锁,且都不释放自身的锁。

public class DeadlockExample {
    // 定义两个共享资源(锁)
    private static final Object LOCK_A = new Object();
    private static final Object LOCK_B = new Object();

    public static void main(String[] args) {
        // 线程1:持有LOCK_A,请求LOCK_B
        new Thread(() -> {
            synchronized (LOCK_A) {
                System.out.println("线程1获得LOCK_A,尝试获取LOCK_B...");
                try {
                    Thread.sleep(100); // 模拟业务操作,放大死锁概率
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (LOCK_B) {
                    System.out.println("线程1获得LOCK_B,执行完成");
                }
            }
        }, "线程1").start();

        // 线程2:持有LOCK_B,请求LOCK_A
        new Thread(() -> {
            synchronized (LOCK_B) {
                System.out.println("线程2获得LOCK_B,尝试获取LOCK_A...");
                try {
                    Thread.sleep(100); // 模拟业务操作,放大死锁概率
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (LOCK_A) {
                    System.out.println("线程2获得LOCK_A,执行完成");
                }
            }
        }, "线程2").start();
    }
}

执行结果

线程1获得LOCK_A,尝试获取LOCK_B...
线程2获得LOCK_B,尝试获取LOCK_A...

程序卡死,两个线程永远无法继续执行。

死锁的检测与定位

在开发和调试阶段,可通过以下工具检测死锁:

JDK 自带工具

  • jps:查看 Java 进程 ID

    jps -l  # 输出进程ID和主类名
  • jstack:打印线程堆栈,检测死锁

    jstack <进程ID>  # 若存在死锁,会显示"Found one Java-level deadlock"

    死锁部分堆栈示例:

    Found one Java-level deadlock:
    =============================
    "线程2":
      waiting to lock monitor 0x00007f8a3a00a888 (object 0x000000076b618610, a java.lang.Object),
      which is held by "线程1"
    "线程1":
      waiting to lock monitor 0x00007f8a3a00d088 (object 0x000000076b618620, a java.lang.Object),
      which is held by "线程2"

代码层面检测

通过ThreadMXBean实时监控死锁:

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;

public class DeadlockDetector {
    public static void main(String[] args) {
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        // 定时检测死锁
        new Thread(() -> {
            while (true) {
                long[] deadlockedThreadIds = threadMXBean.findDeadlockedThreads();
                if (deadlockedThreadIds != null) {
                    System.out.println("检测到死锁,线程ID:" + Arrays.toString(deadlockedThreadIds));
                    // 可进一步打印堆栈信息或触发告警
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

死锁的预防与解决

解决死锁的核心思路是破坏四个必要条件中的至少一个,具体方案如下:

1.破坏 “循环等待条件”(最常用)

方案:为所有资源定义统一的获取顺序,线程必须按顺序获取资源,避免环形等待。

优化示例(修复上述死锁代码):

// 规定获取顺序:先获取LOCK_A,再获取LOCK_B
public class FixedDeadlockExample {
    private static final Object LOCK_A = new Object();
    private static final Object LOCK_B = new Object();

    public static void main(String[] args) {
        // 线程1:按顺序获取LOCK_A → LOCK_B
        new Thread(() -> {
            synchronized (LOCK_A) {
                System.out.println("线程1获得LOCK_A,尝试获取LOCK_B...");
                synchronized (LOCK_B) {
                    System.out.println("线程1获得LOCK_B,执行完成");
                }
            }
        }, "线程1").start();

        // 线程2:同样按顺序获取LOCK_A → LOCK_B(而非先LOCK_B)
        new Thread(() -> {
            synchronized (LOCK_A) {  // 修复点:先获取LOCK_A
                System.out.println("线程2获得LOCK_A,尝试获取LOCK_B...");
                synchronized (LOCK_B) {
                    System.out.println("线程2获得LOCK_B,执行完成");
                }
            }
        }, "线程2").start();
    }
}

2.破坏 “请求与保持条件”

方案:线程获取资源前,先释放已持有的所有资源;或一次性申请所有所需资源,未获取全部则不持有任何资源。

示例

// 一次性申请所有资源,失败则释放已获资源
public class NoHoldLockExample {
    private static final Object LOCK_A = new Object();
    private static final Object LOCK_B = new Object();

    // 尝试同时获取两个锁
    private static boolean tryAcquireBothLocks() {
        synchronized (LOCK_A) {
            if (Thread.holdsLock(LOCK_B)) { // 若已持有LOCK_B,直接返回
                return true;
            }
            synchronized (LOCK_B) {
                return true;
            }
        }
    }

    public static void main(String[] args) {
        new Thread(() -> {
            if (tryAcquireBothLocks()) {
                System.out.println("线程1获取所有锁,执行完成");
            }
        }).start();
    }
}

3.破坏 “不剥夺条件”

方案:使用可中断的锁(如ReentrantLock),允许线程在超时或被中断时释放已持有的锁。

示例

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

public class InterruptibleLockExample {
    private static final Lock LOCK_A = new ReentrantLock();
    private static final Lock LOCK_B = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                if (LOCK_A.tryLock(1, TimeUnit.SECONDS)) { // 尝试获取LOCK_A,超时1秒
                    try {
                        if (LOCK_B.tryLock(1, TimeUnit.SECONDS)) { // 尝试获取LOCK_B,超时1秒
                            try {
                                System.out.println("线程1获取所有锁,执行完成");
                            } finally {
                                LOCK_B.unlock();
                            }
                        } else {
                            System.out.println("线程1获取LOCK_B超时,释放LOCK_A");
                        }
                    } finally {
                        LOCK_A.unlock();
                    }
                }
            } catch (InterruptedException e) {
                System.out.println("线程1被中断,释放资源");
            }
        });

        t1.start();
        // 其他线程逻辑...
    }
}

4. 破坏 “互斥条件”

方案:使用可共享的资源(如ReadWriteLock的读锁),允许多个线程同时访问资源。但该方案仅适用于读多写少的场景,不适用于排他性资源。