在 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 提供的原子类或并发集合。