
在多线程编程中死锁Deadlock是一个经典而又棘手的问题。它如同程序的“交通堵塞”让多个线程互相等待对方释放资源最终谁也无法继续执行导致系统挂起。理解死锁的原理、学会避免和排查死锁是每一位并发程序员的必修课。本文将从死锁的定义开始通过生动的代码示例演示死锁的产生剖析其产生的四个必要条件并给出多种预防和检测方法助你远离死锁的困扰。一、什么是死锁死锁是指两个或两个以上的线程在执行过程中因争夺资源而造成的一种互相等待的现象。若无外力干预它们都将无法推进下去。简单来说就是线程 A 持有资源 1等待资源 2线程 B 持有资源 2等待资源 1。双方都得不到想要的资源于是永远阻塞。二、死锁的四个必要条件死锁的发生必须同时满足以下四个条件缺一不可互斥条件资源一次只能被一个线程占用如果其他线程请求该资源只能等待直到释放。持有并等待条件线程已经持有了至少一个资源又在等待其他资源且不释放已持有的资源。不可剥夺条件线程已获得的资源在未使用完之前不能被其他线程强行剥夺只能由自己主动释放。循环等待条件存在一组线程的循环等待链即 T0 等待 T1 持有的资源T1 等待 T2 持有的资源……Tn 等待 T0 持有的资源。只要破坏其中任何一个条件死锁就不会发生。三、死锁代码示例下面是一个经典的死锁示例两个线程分别持有一个锁并试图获取对方的锁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(); } }运行结果线程1 获得 lock1等待 lock2... 线程2 获得 lock2等待 lock1...之后程序便停滞不前两个线程互相等待形成死锁。四、如何避免死锁根据死锁的四个必要条件我们可以采取相应的预防策略1. 破坏“持有并等待”方法一次性申请所有资源如果申请失败则释放已持有的所有资源。在 Java 中可以使用ReentrantLock的tryLock方法尝试获取锁超时则放弃。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这样就不会出现循环等待。synchronized (lock1) { synchronized (lock2) { // 安全 } }4. 使用更高层次的并发工具例如使用java.util.concurrent包中的Semaphore、CountDownLatch、BlockingQueue等它们内部已经处理好资源竞争减少了手写锁导致死锁的机会。五、如何检测死锁即使代码编写时注意了预防死锁仍可能因复杂逻辑而偶然出现。我们需要学会在运行时检测死锁。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 线程22. JConsole / VisualVM这些图形化工具可以直接连接到 JVM在“线程”选项卡中点击“检测死锁”按钮即可快速定位。3. 编程方式检测通过ThreadMXBean可以在程序中主动检测死锁ThreadMXBean threadMXBean ManagementFactory.getThreadMXBean(); long[] deadlockedThreads threadMXBean.findDeadlockedThreads(); if (deadlockedThreads ! null) { // 有死锁发生可以记录日志或尝试恢复 }六、死锁恢复策略一旦发生死锁通常的恢复手段有重启应用最直接但可能导致数据丢失。杀死其中一个线程强行释放资源但 Java 并没有提供直接杀死线程的安全方法通常通过中断来协作。使用超时机制如tryLock(long, TimeUnit)在等待一定时间后放弃然后释放已持有锁让其他线程有机会执行。七、总结死锁是多线程编程中难以完全避免的问题但通过理解其产生的四个必要条件我们可以有针对性地采用预防策略按顺序申请资源、使用超时锁、减少锁持有时间等。同时掌握 jstack、JConsole 等工具能帮助我们在问题发生后快速定位。