
在 Java 多线程并发编程中死锁是最隐蔽、最棘手的问题之一——它会导致线程永久阻塞、程序卡死且排查难度高一旦线上发生可能造成严重的业务中断。很多开发者只知道“死锁不好”却不知道如何从源头预防本文将从死锁的核心原理、常见场景出发拆解 6 种可落地的预防方案结合代码案例让你既能理解底层逻辑也能在实际开发中直接套用。一、先搞懂死锁是什么为什么会发生1. 死锁的定义死锁是指两个或多个线程互相持有对方所需的锁资源且都不主动释放导致所有线程永久阻塞无法继续执行的状态。简单说就是“你拿着我要的我拿着你要的互相僵持不下”。2. 死锁的4个必要条件缺一不可死锁的发生必须同时满足以下 4 个条件只要破坏其中任意一个就能避免死锁——这也是我们预防死锁的核心逻辑。互斥条件锁资源具有排他性同一时刻只能被一个线程持有如 synchronized 锁、Lock 锁这是并发安全的基础无法破坏。持有并等待条件线程持有一个锁后又去申请其他锁且在等待新锁时不释放已持有的锁。不可剥夺条件线程持有的锁只能由自己主动释放无法被其他线程强制剥夺。循环等待条件多个线程形成闭环每个线程都在等待下一个线程持有的锁如 T1 等 T2 的锁T2 等 T1 的锁。3. 死锁经典案例一看就懂下面这段代码是最典型的死锁场景两个线程互相持有对方需要的锁且都不释放最终卡死。public class DeadlockDemo { // 两个锁资源 private static final Object lock1 new Object(); private static final Object lock2 new Object(); public static void main(String[] args) { // 线程1持有lock1等待lock2 new Thread(() - { synchronized (lock1) { System.out.println(线程1持有lock1等待lock2); try { Thread.sleep(100); // 模拟耗时操作让线程2先持有lock2 } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock2) { // 等待lock2此时线程2已持有lock2 System.out.println(线程1获取lock2执行完成); } } }, 线程1).start(); // 线程2持有lock2等待lock1 new Thread(() - { synchronized (lock2) { System.out.println(线程2持有lock2等待lock1); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock2) { // 此处笔误实际应为lock1模拟死锁 System.out.println(线程2获取lock1执行完成); } } }, 线程2).start(); } }运行结果两个线程会一直打印“持有XX等待XX”之后永久阻塞程序无法终止——这就是死锁。二、核心重点6种死锁预防方案可落地优先选前3种预防死锁的核心思路破坏死锁的4个必要条件除了互斥条件其他3个都可破坏以下方案按“落地难度实用程度”排序优先推荐前3种开发中直接套用即可。方案1按固定顺序获取锁最推荐破坏“循环等待”这是最简单、最有效的预防方案核心逻辑所有线程获取多把锁时都遵循统一的顺序如按锁对象的哈希值排序、按自定义序号排序避免形成闭环等待。优化后的代码解决上面的死锁案例public class DeadlockPrevent1 { private static final Object lock1 new Object(); private static final Object lock2 new Object(); public static void main(String[] args) { // 线程1按 lock1 → lock2 的顺序获取锁 new Thread(() - getLockInOrder(lock1, lock2), 线程1).start(); // 线程2同样按 lock1 → lock2 的顺序获取锁不再颠倒 new Thread(() - getLockInOrder(lock1, lock2), 线程2).start(); } // 统一的锁获取方法按固定顺序获取 private static void getLockInOrder(Object firstLock, Object secondLock) { synchronized (firstLock) { System.out.println(Thread.currentThread().getName() 持有 firstLock.hashCode() 等待 secondLock.hashCode()); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (secondLock) { System.out.println(Thread.currentThread().getName() 获取所有锁执行完成); } } } }核心原理无论哪个线程都先获取 lock1再获取 lock2不会出现“线程1等lock2、线程2等lock1”的闭环彻底破坏“循环等待”条件。实战技巧如果锁对象较多可给每个锁分配一个唯一序号如 1、2、3线程获取锁时严格按序号从小到大或从大到小的顺序获取。方案2一次性获取所有锁破坏“持有并等待”核心逻辑线程在执行任务前一次性申请所有需要的锁资源如果有任意一把锁无法获取就放弃所有已申请的锁等待一段时间后重新尝试——避免“持有部分锁等待其他锁”的情况。适合场景线程需要多把锁才能完成任务且锁的数量固定。代码实现用 Lock 锁实现更灵活import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class DeadlockPrevent2 { // 两个锁资源 private static final Lock lock1 new ReentrantLock(); private static final Lock lock2 new ReentrantLock(); public static void main(String[] args) { new Thread(() - getLockAllAtOnce(), 线程1).start(); new Thread(() - getLockAllAtOnce(), 线程2).start(); } // 一次性获取所有锁 private static void getLockAllAtOnce() { while (true) { // 尝试获取所有锁tryLock() 非阻塞获取失败返回false boolean lock1Acquired lock1.tryLock(); boolean lock2Acquired lock2.tryLock(); // 成功获取所有锁执行任务 if (lock1Acquired lock2Acquired) { try { System.out.println(Thread.currentThread().getName() 获取所有锁执行任务); break; // 任务执行完成退出循环 } finally { // 释放所有锁必须在finally中释放避免锁泄漏 lock1.unlock(); lock2.unlock(); } } else { // 未获取到所有锁释放已获取的锁重新尝试 if (lock1Acquired) { lock1.unlock(); } if (lock2Acquired) { lock2.unlock(); } System.out.println(Thread.currentThread().getName() 未获取到所有锁重试); try { Thread.sleep(50); // 等待一段时间避免频繁重试 } catch (InterruptedException e) { e.printStackTrace(); } } } } }核心亮点用 tryLock() 非阻塞获取锁避免线程阻塞在某一把锁上若未获取全部锁及时释放已持有的锁破坏“持有并等待”条件。方案3给锁设置超时时间破坏“不可剥夺”核心逻辑线程获取锁时设置一个超时时间若超过时间仍未获取到锁就主动释放已持有的锁避免永久等待——相当于“主动让步”破坏“不可剥夺”条件。注意synchronized 锁无法设置超时时间需用 Lock 锁的 tryLock(long timeout, TimeUnit unit) 方法实现。代码实现import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class DeadlockPrevent3 { private static final Lock lock1 new ReentrantLock(); private static final Lock lock2 new ReentrantLock(); public static void main(String[] args) { new Thread(() - getLockWithTimeout(), 线程1).start(); new Thread(() - getLockWithTimeout(), 线程2).start(); } // 带超时时间的锁获取 private static void getLockWithTimeout() { boolean lock1Acquired false; boolean lock2Acquired false; try { // 尝试获取lock1超时时间100ms lock1Acquired lock1.tryLock(100, TimeUnit.MILLISECONDS); if (lock1Acquired) { System.out.println(Thread.currentThread().getName() 获取lock1等待lock2); // 尝试获取lock2超时时间100ms lock2Acquired lock2.tryLock(100, TimeUnit.MILLISECONDS); if (lock2Acquired) { System.out.println(Thread.currentThread().getName() 获取lock2执行任务); } else { System.out.println(Thread.currentThread().getName() 获取lock2超时释放lock1); } } else { System.out.println(Thread.currentThread().getName() 获取lock1超时放弃); } } catch (InterruptedException e) { e.printStackTrace(); } finally { // 释放已获取的锁 if (lock2Acquired) { lock2.unlock(); } if (lock1Acquired) { lock1.unlock(); } } } }核心原理线程获取锁时若超时未获取到就主动释放已持有的锁不会一直持有锁等待从而破坏“不可剥夺”条件避免死锁。方案4使用定时释放锁机制兜底方案核心逻辑给线程设置“守护线程”或“定时任务”当线程持有锁超过指定时间由守护线程强制中断线程释放锁——本质也是破坏“不可剥夺”条件。适用场景无法避免“持有并等待”且锁超时时间不好确定的场景如复杂业务逻辑。注意强制中断线程可能导致数据不一致需在中断后做数据回滚处理谨慎使用。方案5减少锁的持有时间降低死锁概率核心逻辑只在必要的代码段加锁避免长时间持有锁如避免在锁内执行 IO 操作、循环耗时操作减少线程之间的锁竞争间接降低死锁概率。反例错误写法锁内执行 IO 操作持有锁时间过长容易导致死锁。// 错误写法锁内执行IO操作持有锁时间长 synchronized (lock) { // IO操作耗时久 FileWriter writer new FileWriter(test.txt); writer.write(test); writer.close(); // 业务逻辑 }正例正确写法将耗时操作移出锁外缩短锁持有时间。// 正确写法锁只包裹核心业务逻辑 FileWriter writer new FileWriter(test.txt); // 耗时操作移出锁外 synchronized (lock) { // 核心业务逻辑耗时短 count; } writer.write(test); writer.close();方案6使用无锁编程从源头避免核心逻辑尽量避免使用锁改用“无锁数据结构”或“原子类”如 ConcurrentHashMap、AtomicInteger从源头消除死锁的可能——这是最彻底的方案但适用场景有限。示例用 AtomicInteger 替代 synchronized 实现计数器无需加锁自然不会死锁。import java.util.concurrent.atomic.AtomicInteger; public class NoLockDemo { // 原子类无锁实现线程安全 private static final AtomicInteger count new AtomicInteger(0); public static void main(String[] args) { // 多线程操作无需加锁 new Thread(() - { for (int i 0; i 1000; i) { count.incrementAndGet(); // 原子操作线程安全 } }).start(); new Thread(() - { for (int i 0; i 1000; i) { count.decrementAndGet(); } }).start(); System.out.println(最终计数 count.get()); } }适用场景简单的计数器、缓存更新等场景可直接用 JUC 提供的原子类或并发集合。