1. 死锁的定义
死锁是指两个或两个以上的线程,相互持有对方需要的锁,并且都不释放自己手中的锁,导致所有线程都无法继续执行。
通俗例子
两个人面对面过独木桥,你等我让,我等你让,结果谁都过不去。
2. 死锁的四个必要条件(Coffman 条件)
| ① 互斥 | 资源同时只能被一个线程占用 |
| ② 持有并等待 | 线程持有至少一个资源,同时等待其他资源 |
| ③ 不可剥夺 | 线程持有的资源只能由自己释放 |
| ④ 循环等待 | 线程 T1 等 T2,T2 等 T3,…,Tn 等 T1 |
✅ 四个条件缺一不可,只要破坏任意一个,就能避免死锁。
3. 最简单的死锁示例代码
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("Thread1: 持有锁A,等待锁B");
sleep(100);
synchronized (LOCK_B) {
System.out.println("Thread1: 同时持有锁A和锁B");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (LOCK_B) {
System.out.println("Thread2: 持有锁B,等待锁A");
sleep(100);
synchronized (LOCK_A) {
System.out.println("Thread2: 同时持有锁A和锁B");
}
}
});
t1.start();
t2.start();
}
private static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException e) {}
}
}
运行后程序卡住不结束,即发生了死锁。
4. 如何避免死锁(破坏条件)
破坏「循环等待」——最常用
统一锁顺序:所有线程按照相同顺序获取锁。
// 改造后:先锁 A,再锁 B(两个线程一致)
synchronized (LOCK_A) {
synchronized (LOCK_B) {
// 业务代码
}
}
破坏「持有并等待」
使用 tryLock 超时,获取不到则释放已有锁。
if (lockA.tryLock(1, TimeUnit.SECONDS)) {
try {
if (lockB.tryLock(1, TimeUnit.SECONDS)) {
try {
// 业务代码
} finally { lockB.unlock(); }
}
} finally { lockA.unlock(); }
}
破坏「不可剥夺」
使用 Lock 接口,可以让其他线程中断等待锁的线程。
破坏「互斥」(一般不现实)
某些场景可以用 ConcurrentHashMap、Atomic 等无锁替代。
5. 死锁的线上排查方法(实战重点)
方法一:jstack(最常用)
# 1. 找到 Java 进程 ID
jps -l
# 2. 导出线程堆栈
jstack -l <pid>
# 3. 查看输出末尾,JVM 会自动检测死锁:
# "Found one Java-level deadlock"
输出示例:
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x… (object LOCK_A)
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x… (object LOCK_B)
which is held by "Thread-1"
方法二:Arthas(阿里开源神器)
# 启动 arthas
java -jar arthas-boot.jar
# 选择进程后,执行
thread -b
# 输出会直接显示死锁线程和堆栈
方法三:jconsole(图形化)
-
连接进程 → 线程 → 检测死锁
方法四:kill -3(Linux)
kill -3 <pid>
# 会在标准输出/日志中打印线程堆栈
6. 真实案例分析:转账场景死锁
// 账户 A 向 B 转账
void transfer(Account from, Account to, int amount) {
synchronized (from) {
synchronized (to) {
from.debit(amount);
to.credit(amount);
}
}
}
如果同时有两个转账操作:
-
线程1:A→B(锁 A,再锁 B)
-
线程2:B→A(锁 B,再锁 A)
就会产生死锁。
解决方案:对账户对象按 hashCode 排序,统一锁顺序。
Object first = System.identityHashCode(from) < System.identityHashCode(to) ? from : to;
Object second = first == from ? to : from;
synchronized (first) {
synchronized (second) {
// 转账
}
}
7. 追问
Q1:如何写一段一定会死锁的代码?
参考第 3 节的示例代码,两个线程以相反顺序获取两把锁即可。
Q2:死锁与活锁、饥饿的区别?
| 死锁 | 相互等待,不执行 |
| 活锁 | 线程不断改变状态,但仍无法执行(如礼貌地互相谦让) |
| 饥饿 | 某些线程长期得不到锁(如非公平锁导致) |
Q3:数据库中的死锁和 Java 死锁有何不同?
-
数据库死锁:数据库检测到后会主动回滚一个事务
-
Java 死锁:JVM 不会自动处理,需要人工干预

![[linux仓库]线程池(单例模式)、线程安全与重入、死锁[线程·拾]-171主机测评](https://www.171host.com/wp-content/uploads/2026/01/20260128040902-69798bdebfc12-220x150.jpg)
