哈哈,这个题目太真实了——写 Java 的人谁没被死锁坑过几次呢?😅

我帮你梳理一下:


1. 什么是死锁?

在多线程程序中,如果两个(或多个)线程互相等待对方持有的锁而不释放,就会形成死锁。比如:

public class DeadlockDemo {
    private final Object lockA = new Object();
    private final Object lockB = new Object();

    public void method1() {
        synchronized (lockA) {
            System.out.println("Thread " + Thread.currentThread().getName() + " got lockA");
            try { Thread.sleep(100); } catch (InterruptedException ignored) {}
            synchronized (lockB) {
                System.out.println("Thread " + Thread.currentThread().getName() + " got lockB");
            }
        }
    }

    public void method2() {
        synchronized (lockB) {
            System.out.println("Thread " + Thread.currentThread().getName() + " got lockB");
            try { Thread.sleep(100); } catch (InterruptedException ignored) {}
            synchronized (lockA) {
                System.out.println("Thread " + Thread.currentThread().getName() + " got lockA");
            }
        }
    }
}

两个线程一个先拿 A 再拿 B,一个先拿 B 再拿 A,就很容易死锁。


2. 如何发现死锁?

  • JDK 工具
    • jconsole 或 jvisualvm 可以直接检测线程死锁。
    • jstack <pid> 打印线程堆栈,能看到 Found one Java-level deadlock: 提示。
  • 日志定位
    • 在加锁的地方打印线程信息,发现某些线程长期阻塞。

3. 如何避免死锁?

主要有几条原则:

  1. 固定锁顺序
    所有线程获取锁的顺序必须一致,比如大家都先拿 lockA,再拿 lockB
  2. 尝试锁 + 超时
    使用 ReentrantLock.tryLock(timeout, unit),避免无限等待。ReentrantLock lockA = new ReentrantLock(); ReentrantLock lockB = new ReentrantLock(); public void safeMethod() throws InterruptedException { if (lockA.tryLock(100, TimeUnit.MILLISECONDS)) { try { if (lockB.tryLock(100, TimeUnit.MILLISECONDS)) { try { // safe work } finally { lockB.unlock(); } } } finally { lockA.unlock(); } } }
  3. 减少锁的粒度和持有时间
    不要在锁里做 I/O、网络、sleep 等耗时操作。
  4. 使用并发工具类
    java.util.concurrent 里的 ConcurrentHashMapBlockingQueue 等,很多场景可以避免手动加锁。

4. 如果真的遇到了死锁怎么办?

  • 线上紧急处理
    • kill -3 <pid> 打印线程堆栈,确认死锁点。
    • 重启服务(应急手段)。
  • 根本解决
    • 分析死锁的锁顺序,重构代码,统一加锁顺序。
    • 或者改用无锁/更细粒度的数据结构。

好嘞,那我给你一个完整的小示例:

先写一个“必死锁”的 Demo,然后再写一个“改造后的安全版本”。


🔴 死锁示例(必现)

public class DeadlockExample {

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

    // 线程1:先拿 lockA,再拿 lockB
    public void methodA() {
        synchronized (lockA) {
            System.out.println(Thread.currentThread().getName() + " 拿到 lockA");
            try { Thread.sleep(100); } catch (InterruptedException ignored) {}
            synchronized (lockB) {
                System.out.println(Thread.currentThread().getName() + " 拿到 lockB");
            }
        }
    }

    // 线程2:先拿 lockB,再拿 lockA
    public void methodB() {
        synchronized (lockB) {
            System.out.println(Thread.currentThread().getName() + " 拿到 lockB");
            try { Thread.sleep(100); } catch (InterruptedException ignored) {}
            synchronized (lockA) {
                System.out.println(Thread.currentThread().getName() + " 拿到 lockA");
            }
        }
    }

    public static void main(String[] args) {
        DeadlockExample example = new DeadlockExample();

        Thread t1 = new Thread(example::methodA, "线程1");
        Thread t2 = new Thread(example::methodB, "线程2");

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

👉 运行后你会发现输出类似:

线程1 拿到 lockA
线程2 拿到 lockB

然后就卡住了,再也没有后续输出,这就是死锁


✅ 改造后的安全版本(固定锁顺序)

public class FixedLockExample {

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

    // 所有线程都严格先拿 lockA,再拿 lockB
    public void methodSafe(String name) {
        synchronized (lockA) {
            System.out.println(name + " 拿到 lockA");
            try { Thread.sleep(100); } catch (InterruptedException ignored) {}
            synchronized (lockB) {
                System.out.println(name + " 拿到 lockB");
            }
        }
    }

    public static void main(String[] args) {
        FixedLockExample example = new FixedLockExample();

        Thread t1 = new Thread(() -> example.methodSafe("线程1"));
        Thread t2 = new Thread(() -> example.methodSafe("线程2"));

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

👉 这次运行不会死锁,因为大家都遵循同一个加锁顺序。


✅ 改造后的安全版本(ReentrantLock + tryLock 超时)

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

public class TryLockExample {

    private final ReentrantLock lockA = new ReentrantLock();
    private final ReentrantLock lockB = new ReentrantLock();

    public void safeMethod(String name) {
        try {
            if (lockA.tryLock(100, TimeUnit.MILLISECONDS)) {
                try {
                    if (lockB.tryLock(100, TimeUnit.MILLISECONDS)) {
                        try {
                            System.out.println(name + " 成功拿到 lockA 和 lockB");
                        } finally {
                            lockB.unlock();
                        }
                    } else {
                        System.out.println(name + " 拿 lockB 超时,放弃");
                    }
                } finally {
                    lockA.unlock();
                }
            } else {
                System.out.println(name + " 拿 lockA 超时,放弃");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    public static void main(String[] args) {
        TryLockExample example = new TryLockExample();

        Thread t1 = new Thread(() -> example.safeMethod("线程1"));
        Thread t2 = new Thread(() -> example.safeMethod("线程2"));

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

👉 即使竞争同一把锁,也不会“死等”,最多超时报错退出,避免死锁。