死锁(Deadlock)是指多个线程互相持有对方需要的资源,导致所有线程都无法继续执行的情况。Java 中可以通过以下方法避免死锁:
造成死锁的⼏个原因:
⼀个资源每次只能被⼀个线程使⽤
⼀个线程在阻塞等待某个资源时,不释放已占有资源
⼀个线程已经获得的资源,在未使⽤完之前,不能被强⾏剥夺
若⼲线程形成头尾相接的循环等待资源关系
这是造成死锁必须要达到的4个条件,如果要避免死锁,只需要不满⾜其中某⼀个条件即可。⽽其中前3 个条件是作为锁要符合的条件,所以要避免死锁就需要打破第4个条件,不出现循环等待锁的关系。
在开发过程中:
要注意加锁顺序,保证每个线程按同样的顺序进⾏加锁
要注意加锁时限,可以针对所设置⼀个超时时间
要注意死锁检查,这是⼀种预防机制,确保在第⼀时间发现死锁并进⾏解决
1. 死锁产生的四个必要条件
要避免死锁,首先要理解死锁发生的条件(必须全部满足):
互斥条件:资源一次只能被一个线程占用。
占有并等待:线程持有资源的同时,等待其他资源。
不可抢占:线程持有的资源不能被其他线程强行夺走。
循环等待:多个线程形成环形等待链(A 等 B,B 等 C,C 等 A)。
只要破坏其中任意一个条件,就能避免死锁!
2. 避免死锁的 5 种方法
方法 1:按固定顺序获取锁(破坏循环等待)
核心思想:所有线程按相同的顺序申请锁,避免循环依赖。
✅ 正确示例:
public void transfer(Account from, Account to, int amount) {
// 规定:先锁 id 小的账户,再锁 id 大的账户
Account first = from.getId() < to.getId() ? from : to;
Account second = from.getId() < to.getId() ? to : from;
synchronized (first) {
synchronized (second) {
if (from.getBalance() >= amount) {
from.debit(amount);
to.credit(amount);
}
}
}
}
为什么能避免死锁?
所有线程都按 id 顺序加锁,不会出现 A 锁 1 → 等 2 和 B 锁 2 → 等 1 的情况。
方法 2:使用 tryLock + 超时(破坏占有并等待)
核心思想:如果拿不到锁,就释放已持有的锁,避免无限等待。
✅ 正确示例(ReentrantLock):
public boolean transfer(Account from, Account to, int amount, long timeout) throws InterruptedException {
long startTime = System.currentTimeMillis();
while (true) {
if (from.lock.tryLock()) { // 尝试获取第一把锁
try {
if (to.lock.tryLock()) { // 尝试获取第二把锁
try {
if (from.getBalance() >= amount) {
from.debit(amount);
to.credit(amount);
return true;
}
} finally {
to.lock.unlock();
}
}
} finally {
from.lock.unlock(); // 释放第一把锁
}
}
// 超时检查
if (System.currentTimeMillis() - startTime >= timeout) {
return false;
}
Thread.sleep(100); // 避免 CPU 忙等待
}
}
优点:
不会无限等待,超时后可以重试或回滚。
适用于高并发场景(如支付系统)。
方法 3:一次性申请所有资源(破坏占有并等待)
核心思想:使用一个全局锁,一次性申请所有需要的资源。
✅ 正确示例:
public class AccountManager {
private static final Object globalLock = new Object();
public void transfer(Account from, Account to, int amount) {
synchronized (globalLock) { // 全局锁保护所有账户
if (from.getBalance() >= amount) {
from.debit(amount);
to.credit(amount);
}
}
}
}
缺点:并发度降低(所有转账操作串行化)。
方法 4:使用 Lock 替代 synchronized(更灵活的控制)
ReentrantLock 比 synchronized 更灵活,可以避免死锁:
Lock lock1 = new ReentrantLock();
Lock lock2 = new ReentrantLock();
public void methodA() {
boolean gotLock1 = false;
boolean gotLock2 = false;
try {
gotLock1 = lock1.tryLock(1, TimeUnit.SECONDS);
gotLock2 = lock2.tryLock(1, TimeUnit.SECONDS);
if (gotLock1 && gotLock2) {
// 执行业务逻辑
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (gotLock1) lock1.unlock();
if (gotLock2) lock2.unlock();
}
}
方法 5:检测与恢复(允许死锁发生,但能自动恢复)
适用于复杂系统(如数据库、分布式系统):
1.检测死锁:
使用 ThreadMXBean 查找死锁:
ThreadMXBean bean = ManagementFactory.getThreadMXBean();
long[] threadIds = bean.findDeadlockedThreads();
if (threadIds != null) {
System.out.println("检测到死锁!");
}
2.恢复策略:
强制终止某个线程(如 thread.interrupt())。
回滚事务(数据库场景)。
3. 死锁案例分析
❌ 错误代码(会导致死锁)
public void transfer(Account from, Account to, int amount) {
synchronized (from) { // 线程1:锁 from → 等 to
synchronized (to) { // 线程2:锁 to → 等 from
if (from.getBalance() >= amount) {
from.debit(amount);
to.credit(amount);
}
}
}
}
死锁场景:
线程1:transfer(accountA, accountB, 100)
线程2:transfer(accountB, accountA, 200)
结果:互相等待,死锁!
4. 总结
方法适用场景优点缺点固定顺序加锁简单锁依赖实现简单需要全局排序规则tryLock + 超时高并发系统避免无限等待代码复杂度高全局锁低并发场景绝对安全性能差(串行化)Lock 替代 synchronized需要更细粒度控制灵活(可中断、超时)需手动释放锁检测与恢复数据库、分布式系统适用于复杂场景实现复杂
最佳实践:
尽量用 tryLock + 超时(推荐 ReentrantLock)。
如果必须用 synchronized,按固定顺序加锁。
避免嵌套锁(如 synchronized 里再调 synchronized 方法)。
使用工具检测死锁(如 jstack、ThreadMXBean)。