Java 死锁详解:从原理到预防与解决

1. 什么是死锁?

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象。如果没有外部干预,这些线程将无法继续执行下去,程序会陷入永久的阻塞状态。

1.1 生活中的死锁比喻

想象这样一个场景:

  • 张三拿着车钥匙,等着李四的手机来打电话
  • 李四拿着手机,等着张三的车钥匙去开车
  • 两人互相等待,谁也不肯先放手,这就形成了死锁

2. 死锁产生的四个必要条件

死锁的产生必须同时满足以下四个条件:

2.1 互斥条件

一个资源每次只能被一个线程使用。

java

// 比如 synchronized 关键字或 Lock 锁
public synchronized void method() {
    // 同一时间只能有一个线程进入
}

2.2 请求与保持条件

一个线程因请求资源而阻塞时,对已获得的资源保持不放。

java

// 线程持有锁A,同时请求锁B
synchronized(lockA) {
    synchronized(lockB) {
        // ...
    }
}

2.3 不剥夺条件

线程已获得的资源,在未使用完之前,不能强行被剥夺。

2.4 循环等待条件

若干线程之间形成一种头尾相接的循环等待资源关系。

java

// 线程1:lockA -> lockB
// 线程2:lockB -> lockA

3. Java 中死锁的代码示例

3.1 经典死锁示例

java

public class ClassicDeadlockExample {
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Thread1: Holding lock1...");

                try {
                    Thread.sleep(100); // 模拟业务操作
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }

                System.out.println("Thread1: Waiting for lock2...");
                synchronized (lock2) {
                    System.out.println("Thread1: Acquired lock2!");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("Thread2: Holding lock2...");

                try {
                    Thread.sleep(100); // 模拟业务操作
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }

                System.out.println("Thread2: Waiting for lock1...");
                synchronized (lock1) {
                    System.out.println("Thread2: Acquired lock1!");
                }
            }
        });

        thread1.start();
        thread2.start();

        // 等待线程结束
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        System.out.println("程序执行完毕"); // 这行代码可能永远不会执行
    }
}

3.2 使用 ReentrantLock 的死锁示例

java

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

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

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            lock1.lock();
            System.out.println("Thread1: Acquired lock1");

            try {
                Thread.sleep(100); // 模拟业务操作

                System.out.println("Thread1: Trying to acquire lock2");
                lock2.lock(); // 这里会阻塞,等待lock2

                try {
                    System.out.println("Thread1: Acquired both locks");
                } finally {
                    lock2.unlock();
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                lock1.unlock();
            }
        });

        Thread thread2 = new Thread(() -> {
            lock2.lock();
            System.out.println("Thread2: Acquired lock2");

            try {
                Thread.sleep(100); // 模拟业务操作

                System.out.println("Thread2: Trying to acquire lock1");
                lock1.lock(); // 这里会阻塞,等待lock1

                try {
                    System.out.println("Thread2: Acquired both locks");
                } finally {
                    lock1.unlock();
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                lock2.unlock();
            }
        });

        thread1.start();
        thread2.start();
    }
}

4. 检测 Java 程序中的死锁

4.1 使用 jstack 工具检测

bash

# 1. 找到 Java 进程的 PID
jps

# 2. 使用 jstack 分析线程状态
jstack <PID>

# 或者一步到位
jstack <PID> | grep -A 10 -B 10 "deadlock"

4.2 编程方式检测死锁

java

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

public class DeadlockDetector {
    public static void main(String[] args) {
        // 启动死锁检测线程
        Thread detectorThread = new Thread(() -> {
            while (true) {
                checkDeadlock();
                try {
                    Thread.sleep(5000); // 每5秒检测一次
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
        });
        detectorThread.setDaemon(true);
        detectorThread.start();

        // 运行可能产生死锁的代码
        ClassicDeadlockExample.main(args);
    }

    public static void checkDeadlock() {
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        long[] threadIds = threadMXBean.findDeadlockedThreads();

        if (threadIds != null) {
            System.out.println("检测到死锁!涉及线程:" + threadIds.length);

            // 获取线程信息
            java.lang.management.ThreadInfo[] threadInfos = 
            threadMXBean.getThreadInfo(threadIds);

            for (java.lang.management.ThreadInfo threadInfo : threadInfos) {
                System.out.println("死锁线程: " + threadInfo.getThreadName());
                System.out.println("阻塞的锁: " + threadInfo.getLockName());
                System.out.println("阻塞的持有者: " + threadInfo.getLockOwnerName());
                System.out.println("堆栈跟踪:");
                for (StackTraceElement element : threadInfo.getStackTrace()) {
                    System.out.println("    " + element);
                }
                System.out.println("---");
            }
        } else {
            System.out.println("未检测到死锁");
        }
    }
}

5. 预防和解决死锁的策略

5.1 破坏循环等待条件 - 锁顺序化

java

public class LockOrderingSolution {
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    // 定义锁的获取顺序
    private static void acquireLocks(Object firstLock, Object secondLock) {
        // 确保总是先获取哈希值较小的锁
        Object lockA = firstLock.hashCode() < secondLock.hashCode() ? firstLock : secondLock;
        Object lockB = firstLock.hashCode() < secondLock.hashCode() ? secondLock : firstLock;

        synchronized (lockA) {
            synchronized (lockB) {
                // 执行业务逻辑
                System.out.println(Thread.currentThread().getName() + " 成功获取两个锁");
            }
        }
    }

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            acquireLocks(lock1, lock2);
        });

        Thread thread2 = new Thread(() -> {
            acquireLocks(lock2, lock1); // 顺序会被内部方法重新排列
        });

        thread1.start();
        thread2.start();
    }
}

5.2 使用 tryLock 避免死锁

java

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

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

    public static void method1() {
        while (true) {
            if (lock1.tryLock()) {
                try {
                    System.out.println("Thread1: Acquired lock1");

                    if (lock2.tryLock()) {
                        try {
                            System.out.println("Thread1: Acquired both locks - success!");
                            break; // 成功获取两个锁,退出循环
                        } finally {
                            lock2.unlock();
                        }
                    }
                } finally {
                    lock1.unlock();
                }
            }

            // 获取失败,稍后重试
            try {
                Thread.sleep(100 + (int)(Math.random() * 100)); // 随机等待避免活锁
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
    }

    public static void method2() {
        while (true) {
            if (lock2.tryLock()) {
                try {
                    System.out.println("Thread2: Acquired lock2");

                    if (lock1.tryLock()) {
                        try {
                            System.out.println("Thread2: Acquired both locks - success!");
                            break; // 成功获取两个锁,退出循环
                        } finally {
                            lock1.unlock();
                        }
                    }
                } finally {
                    lock2.unlock();
                }
            }

            // 获取失败,稍后重试
            try {
                Thread.sleep(100 + (int)(Math.random() * 100)); // 随机等待避免活锁
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
    }
}

5.3 使用超时机制

java

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

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

    public static boolean tryAcquireLocksWithTimeout() {
        boolean acquiredLock1 = false;
        boolean acquiredLock2 = false;

        try {
            // 尝试在指定时间内获取锁
            acquiredLock1 = lock1.tryLock(1, TimeUnit.SECONDS);
            if (acquiredLock1) {
                acquiredLock2 = lock2.tryLock(1, TimeUnit.SECONDS);
                if (acquiredLock2) {
                    return true; // 成功获取两个锁
                }
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            // 如果没能获取全部锁,释放已获取的锁
            if (acquiredLock1 && !acquiredLock2) {
                lock1.unlock();
            }
            if (acquiredLock2 && !acquiredLock1) {
                lock2.unlock();
            }
        }

        return false;
    }
}

6. 高级死锁处理模式

6.1 锁分解和锁分段

java

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

public class LockStrippingExample {
    // 传统的粗粒度锁
    private final Object coarseLock = new Object();
    private int value1, value2;

    // 锁分解:为不同资源使用不同的锁
    private final Lock lock1 = new ReentrantLock();
    private final Lock lock2 = new ReentrantLock();

    public void updateValue1(int newValue) {
        lock1.lock();
        try {
            value1 = newValue;
        } finally {
            lock1.unlock();
        }
    }

    public void updateValue2(int newValue) {
        lock2.lock();
        try {
            value2 = newValue;
        } finally {
            lock2.unlock();
        }
    }

    // 锁分段:用于HashMap等并发集合
    private static final int STRIPES = 16;
    private final Lock[] stripes = new ReentrantLock[STRIPES];

    public LockStrippingExample() {
        for (int i = 0; i < STRIPES; i++) {
            stripes[i] = new ReentrantLock();
        }
    }

    private int hash(Object key) {
        return Math.abs(key.hashCode() % STRIPES);
    }

    public void put(String key, String value) {
        int stripe = hash(key);
        stripes[stripe].lock();
        try {
            // 执行put操作
            System.out.println("Putting " + key + " in stripe " + stripe);
        } finally {
            stripes[stripe].unlock();
        }
    }
}

6.2 使用线程池和任务队列

java

import java.util.concurrent.*;

public class ExecutorDeadlockAvoidance {
    private static final ExecutorService executor = Executors.newFixedThreadPool(2);

    public static void main(String[] args) throws Exception {
        // 提交相互依赖的任务,但通过线程池避免死锁
        Future<String> future1 = executor.submit(() -> {
            System.out.println("Task1 started");
            // 模拟长时间运行的任务
            Thread.sleep(1000);
            return "Result1";
        });

        Future<String> future2 = executor.submit(() -> {
            System.out.println("Task2 started");
            // 第二个任务不依赖第一个任务的结果
            Thread.sleep(500);
            return "Result2";
        });

        // 获取结果 - 这里不会死锁,因为任务在并行执行
        String result1 = future1.get();
        String result2 = future2.get();

        System.out.println("Results: " + result1 + ", " + result2);
        executor.shutdown();
    }
}

7. 最佳实践和代码规范

7.1 锁获取顺序规范

java

public class LockOrderingBestPractice {
    private final Object resourceA = new Object();
    private final Object resourceB = new Object();
    private final Object resourceC = new Object();

    // 定义明确的锁获取顺序
    private static final int ORDER_A = 1;
    private static final int ORDER_B = 2;
    private static final int ORDER_C = 3;

    private void acquireLocksInOrder(Object... locks) {
        // 按照预定义顺序排序锁
        Arrays.sort(locks, (lock1, lock2) -> {
            int order1 = getLockOrder(lock1);
            int order2 = getLockOrder(lock2);
            return Integer.compare(order1, order2);
        });

        // 按顺序获取锁
        for (Object lock : locks) {
            synchronized (lock) {
                // 锁已获取
            }
        }
    }

    private int getLockOrder(Object lock) {
        if (lock == resourceA) return ORDER_A;
        if (lock == resourceB) return ORDER_B;
        if (lock == resourceC) return ORDER_C;
        throw new IllegalArgumentException("Unknown lock: " + lock);
    }
}

7.2 使用静态分析工具

在项目中集成死锁检测工具:

xml

<!-- Maven 依赖 -->
<dependency>
  <groupId>com.github.spotbugs</groupId>
  <artifactId>spotbugs</artifactId>
  <version>4.2.2</version>
</dependency>

使用 FindBugs/SpotBugs、PMD 等静态分析工具可以在编译期检测潜在的死锁风险。

8. 总结

死锁是多线程编程中的经典问题,理解其产生原理和解决方法对于编写健壮的并发程序至关重要:

  1. 理解四个必要条件:互斥、请求与保持、不剥夺、循环等待
  2. 掌握检测工具:jstack、ThreadMXBean
  3. 应用预防策略:锁顺序化、tryLock、超时机制
  4. 遵循最佳实践:锁分解、避免嵌套锁、使用高层并发工具

通过合理的设计和编码规范,可以有效地避免和解决死锁问题,构建高性能、高可用的并发应用程序。

记住:预防死锁的最佳时机是在设计阶段,而不是在出现问题之后!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐