一、什么是 Java 死锁?
用通俗的话来说:死锁就像两个人吃饭,A 拿着筷子 1,等着要 A 的筷子 2;B 拿着筷子 2,等着要 A 的筷子 1,两个人都不放手,也都吃不上饭,僵持不下。
在 Java 中,死锁是指两个或多个线程在执行过程中,因互相持有对方所需的锁资源,而陷入永久阻塞的状态,彼此都无法继续执行。
二、死锁产生的 4 个必要条件(缺一不可)
只有同时满足以下 4 个条件,才会发生死锁,理解这一点是解决死锁的关键:
- 互斥条件:锁资源只能被一个线程持有,不能被多个线程共享(比如 Java 的 synchronized 锁就是互斥的)。
- 占有且等待:线程已经持有了一个锁,同时又去请求另一个被其他线程持有的锁,且在请求过程中不释放已持有的锁。
- 不可剥夺条件:线程持有的锁不能被其他线程强制夺走,只能由线程主动释放。
- 循环等待条件:多个线程形成环形的锁等待链,比如线程 A 等线程 B 的锁,线程 B 等线程 C 的锁,线程 C 等线程 A 的锁。
三、模拟死锁的代码(可直接运行)
下面是一个简单的死锁示例,帮你直观看到死锁的发生:
java
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
Thread thread1 = new Thread(() -> {
synchronized (LOCK1) {
System.out.println(Thread.currentThread().getName() + " 持有LOCK1,等待LOCK2");
try {
// 休眠1秒,确保线程2先拿到LOCK2,触发死锁
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (LOCK2) {
System.out.println(Thread.currentThread().getName() + " 拿到LOCK2,执行完成");
}
}
}, "线程1");
// 线程2:先拿LOCK2,再尝试拿LOCK1
Thread thread2 = new Thread(() -> {
synchronized (LOCK2) {
System.out.println(Thread.currentThread().getName() + " 持有LOCK2,等待LOCK1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (LOCK1) {
System.out.println(Thread.currentThread().getName() + " 拿到LOCK1,执行完成");
}
}
}, "线程2");
// 启动两个线程
thread1.start();
thread2.start();
}
}
运行结果:
plaintext
线程1 持有LOCK1,等待LOCK2
线程2 持有LOCK2,等待LOCK1
程序会一直卡在这里,两个线程都无法继续执行,死锁发生。
四、如何避免 / 解决死锁?
核心思路:打破死锁的 4 个必要条件中的任意一个即可,常用方法如下:
1. 按固定顺序获取锁(最常用)
打破 "循环等待条件":所有线程都按照相同的顺序获取锁,比如都先拿 LOCK1,再拿 LOCK2。
修改上面的代码,让线程 2 也先拿 LOCK1 再拿 LOCK2,死锁就会消失:
java
// 修复后的线程2代码
Thread thread2 = new Thread(() -> {
synchronized (LOCK1) { // 改为先拿LOCK1
System.out.println(Thread.currentThread().getName() + " 持有LOCK1,等待LOCK2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (LOCK2) {
System.out.println(Thread.currentThread().getName() + " 拿到LOCK2,执行完成");
}
}
}, "线程2");
2. 尝试获取锁时设置超时
打破 "占有且等待条件":使用Lock接口的tryLock(long time, TimeUnit unit)方法,在指定时间内拿不到锁就放弃,并释放已持有的锁。
示例代码:
java
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class AvoidDeadLockWithTryLock {
private static final Lock LOCK1 = new ReentrantLock();
private static final Lock LOCK2 = new ReentrantLock();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
if (LOCK1.tryLock()) { // 尝试获取LOCK1
try {
System.out.println("线程1 拿到LOCK1,等待LOCK2");
Thread.sleep(1000);
// 尝试获取LOCK2,超时1秒就放弃
if (LOCK2.tryLock(1, TimeUnit.SECONDS)) {
try {
System.out.println("线程1 拿到LOCK2,执行完成");
} finally {
LOCK2.unlock(); // 释放LOCK2
}
} else {
System.out.println("线程1 拿LOCK2超时,放弃");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
LOCK1.unlock(); // 释放LOCK1
}
}
}, "线程1");
Thread thread2 = new Thread(() -> {
if (LOCK2.tryLock()) {
try {
System.out.println("线程2 拿到LOCK2,等待LOCK1");
Thread.sleep(1000);
if (LOCK1.tryLock(1, TimeUnit.SECONDS)) {
try {
System.out.println("线程2 拿到LOCK1,执行完成");
} finally {
LOCK1.unlock();
}
} else {
System.out.println("线程2 拿LOCK1超时,放弃");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
LOCK2.unlock();
}
}
}, "线程2");
thread1.start();
thread2.start();
}
}
3. 其他方法
- 减少锁的嵌套:尽量避免一个线程同时持有多个锁,只在必要的代码块加锁。
- 释放已持有的锁:如果拿不到新锁,就主动释放已持有的锁,稍后重试。
五、如何排查 Java 死锁?
当程序发生死锁时,可以用 JDK 自带的工具快速定位:
-
步骤 1:用 jps 命令找到进程 ID
打开终端,执行:
bashjps输出示例(假设 DeadLockDemo 的进程 ID 是 1234):
plaintext1234 DeadLockDemo 5678 Jps步骤 2:用 jstack 命令查看死锁
执行:
bashjstack 1234输出中会明确标注
Found one Java-level deadlock:,并列出死锁的线程、持有的锁和等待的锁,帮你快速定位问题。
总结
- Java 死锁是多线程因互相持有对方所需的锁,陷入永久阻塞的状态,需同时满足互斥、占有且等待、不可剥夺、循环等待4 个条件。
- 避免死锁的核心是打破任意一个必要条件,最常用的是按固定顺序获取锁 和tryLock 超时机制。
- 排查死锁可使用 JDK 自带的
jps + jstack命令,能快速定位死锁的线程和锁资源。