一文搞懂 Java 死锁:四大必要条件、经典死锁 Demo、如何用代码和思想避免死锁

5795 / 2026-07-14 02:51:06 新服预约

死锁(Deadlock)是 Java 并发中最容易踩坑,也最容易被忽视的问题。

它不是异常,不会报错,但会让线程永久卡住,系统彻底失去响应。

这一篇文章,从 死锁原理 → 复现 → 避免策略 做一个完整讲解。

1. 什么是死锁?一句话概念

死锁 = 多个线程互相等待对方持有的资源(锁),形成循环等待,导致程序永久卡死。

示意:

T1 持有锁 A,等待锁 B

T2 持有锁 B,等待锁 A

双方互相等待 → 永久挂死

2. 死锁的“四大必要条件”(必须背)

只要这四个条件同时满足,必死锁:

互斥:资源(如锁)一次只能被一个线程占有

占有并等待:线程已获取锁 A,还要等待锁 B

不可抢占:锁不能被强制夺走

循环等待:多个线程之间互相等待形成环路

只要破坏任意一个条件,死锁就不会发生。

3. 最经典的 Java 死锁 Demo(必看)

public class DeadlockDemo {

private static final Object LOCK_A = new Object();

private static final Object LOCK_B = new Object();

public static void main(String[] args) {

Thread t1 = new Thread(() -> {

synchronized (LOCK_A) {

System.out.println("T1 拿到 A");

try { Thread.sleep(100); } catch (Exception ignored) {}

System.out.println("T1 想要 B...");

synchronized (LOCK_B) {

System.out.println("T1 拿到 B");

}

}

});

Thread t2 = new Thread(() -> {

synchronized (LOCK_B) {

System.out.println("T2 拿到 B");

try { Thread.sleep(100); } catch (Exception ignored) {}

System.out.println("T2 想要 A...");

synchronized (LOCK_A) {

System.out.println("T2 拿到 A");

}

}

});

t1.start();

t2.start();

}

}

运行结果:

T1 拿到 A

T2 拿到 B

T1 想要 B...

T2 想要 A...

(程序卡住不动)

4. 死锁是怎么形成的?(图解一次性看懂)

Thread T1: 拿到锁 A → 想要锁 B(被 T2 占有)

Thread T2: 拿到锁 B → 想要锁 A(被 T1 占有)

形成循环等待:

T1 → 等待 B → 被 T2 持有

T2 → 等待 A → 被 T1 持有

这时:

谁都不能继续

谁都不会释放锁

程序永久卡死

5. ReentrantLock 一样能死锁(换锁不解决死锁问题)

把 synchronized 换成 ReentrantLock,也会死锁:

ReentrantLock lockA = new ReentrantLock();

ReentrantLock lockB = new ReentrantLock();

Thread t1 = new Thread(() -> {

lockA.lock();

try {

Thread.sleep(100);

lockB.lock(); // 可能死锁

try { } finally { lockB.unlock(); }

} catch (Exception ignored) {

} finally {

lockA.unlock();

}

});

死锁不是 synchronized 的缺陷,也不是 ReentrantLock 的缺陷。

死锁来源于“加锁顺序不一致”。

6. 用 ReentrantLock 的 tryLock 避免死锁(极其有效)

synchronized 不支持“尝试加锁”,只能一直等。ReentrantLock 支持 tryLock(),拿不到锁可以直接放弃,避免无限等待。

示例:

if (lockA.tryLock(500, TimeUnit.MILLISECONDS)) {

try {

if (lockB.tryLock(500, TimeUnit.MILLISECONDS)) {

try {

// 成功获取两把锁

} finally {

lockB.unlock();

}

} else {

System.out.println("获取 B 超时,避免死锁");

}

} finally {

lockA.unlock();

}

}

只要在超时时间内做“放弃操作”,就能有效破坏循环等待 → 避免死锁。

7. 实战中避免死锁的 5 条原则(必须记住)

✔ 1. 所有线程必须使用一致的加锁顺序(最重要)

例如规定:

所有地方 → 必须先锁 A,再锁 B

就不会发生:

线程1:A → B

线程2:B → A

✔ 2. 尽量减少锁的数量

锁越多,越容易死锁

尽量减少嵌套锁

✔ 3. 不要在锁内做耗时操作

包括:

sleep

I/O

数据库操作

网络调用

耗时越长,被其他线程等待的时间越长,死锁几率越高。

✔ 4. 多用 ReentrantLock 的 tryLock + 超时机制

逻辑思路是:

拿不到锁 → 马上放弃

不形成无限等待

这可以破坏死锁的关键条件“循环等待”。

✔ 5. 不要混用不同的锁去保护同一资源

例如:

一会儿 synchronized(this)

一会儿 lock.lock()

这种会导致不可控锁顺序,风险极大。

8. 总结:一篇搞懂死锁所有关键点

最后把重点再浓缩成简洁版,方便你记忆:

✦ 死锁是什么?

线程之间互相等待,形成循环依赖,程序永久卡死。

✦ 死锁的四大必要条件

互斥

占有并等待

不可抢占

循环等待

✦ synchronized 会死锁吗?

会。

ReentrantLock 会死锁吗?

也会。

因为死锁是锁顺序问题,不是锁类型问题。

✦ 怎么避免死锁?

统一加锁顺序(最有效)

减少锁数量

锁内不要做耗时任务

使用 tryLock 超时机制

避免混用锁

写在最后

理解死锁,不是为了知道“它怎么发生”,

而是为了在设计系统时主动避免死锁。

“可重入锁解决自己锁自己,死锁解决互相等待。

死锁的根源不是 synchronized,而是程序设计。”