理解死锁在 Java 多线程中的定义与成因
掌握死锁发生的典型场景及代码示例
学会使用 JDK 工具检测运行中的死锁
了解预防和减少死锁发生的常用策略
在 Java 多线程编程中,死锁(Deadlock) 是指两个或多个线程因相互等待对方持有的锁而永久阻塞,导致程序无法继续执行的状态。
具体表现为:
每个线程都持有一个锁,并试图获取另一个被其他线程持有的锁;
形成循环等待,所有相关线程都无法继续推进;
程序“冻结”,但并未崩溃,只是陷入无限等待。
以下是一个典型的 Java 死锁演示程序:
// 工具类:用于暂停线程
class Util {
static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 被两个线程共享的类
class Shared {
// 同步方法一
synchronized void test1(Shared other) {
System.out.println(Thread.currentThread().getName()
+ " 进入 " + this + " 的 test1 方法");
Util.sleep(1000);
// 尝试调用另一个对象的 test2 方法
other.test2();
System.out.println(Thread.currentThread().getName()
+ " 退出 " + this + " 的 test1 方法");
}
// 同步方法二
synchronized void test2() {
System.out.println(Thread.currentThread().getName()
+ " 进入 " + this + " 的 test2 方法");
Util.sleep(1000);
System.out.println(Thread.currentThread().getName()
+ " 退出 " + this + " 的 test2 方法");
}
}
class WorkerThread1 extends Thread {
private Shared s1, s2;
public WorkerThread1(Shared s1, Shared s2) {
this.s1 = s1;
this.s2 = s2;
}
@Override
public void run() {
s1.test1(s2);
}
}
class WorkerThread2 extends Thread {
private Shared s1, s2;
public WorkerThread2(Shared s1, Shared s2) {
this.s1 = s1;
this.s2 = s2;
}
@Override
public void run() {
s2.test1(s1);
}
}
public class DeadlockDemo {
public static void main(String[] args) {
Shared s1 = new Shared();
Shared s2 = new Shared();
WorkerThread1 t1 = new WorkerThread1(s1, s2);
t1.setName("线程 A");
t1.start();
WorkerThread2 t2 = new WorkerThread2(s1, s2);
t2.setName("线程 B");
t2.start();
Util.sleep(2000); // 等待足够时间观察死锁
}
}执行流程说明
线程 A 获取 s1 对象的锁,进入 s1.test1(s2);
线程 B 获取 s2 对象的锁,进入 s2.test1(s1);
在 test1 中,线程 A 尝试调用 s2.test2(),但 s2 的锁已被线程 B 持有;
同时,线程 B 尝试调用 s1.test2(),但 s1 的锁已被线程 A 持有;
两者互相等待,形成死锁,程序永久挂起。
注意:该程序不应在在线 IDE 中运行,因为它会导致进程卡死。建议在本地环境测试。
Java 使用内置锁,也称为监视器锁,通过 synchronized 关键字实现。每个对象都有一个与之关联的锁。
当一个线程进入 synchronized 方法或代码块时,它必须先获得该对象的锁;执行完毕后自动释放锁。

上图展示了典型的死锁环路:T1 持有 s1 并等待 s2,T2 持有 s2 并等待 s1。
Java 提供了强大的命令行工具来检测运行中的死锁:
jps -l输出示例:
12345 DeadlockDemo
67890 SomeOtherAppjcmd 12345 Thread.print(将 12345 替换为实际的进程 ID)
在输出中,若存在死锁,JVM 会明确提示:
Found one Java-level deadlock:
=============================
"线程 A":
waiting to lock monitor 0x00007f... (object s2)
which is held by "线程 B"
"线程 B":
waiting to lock monitor 0x00007f... (object s1)
which is held by "线程 A"此外,也可以使用 jstack <PID> 命令生成线程转储(Thread Dump)进行分析。
虽然无法完全消除死锁的可能性,但可通过以下策略显著降低其发生概率:
不要在已经持有某个锁的情况下再去请求另一个锁;
如果必须获取多个锁,确保所有线程以相同的顺序获取锁。
推荐做法:
// 所有线程先获取 s1,再获取 s2
synchronized (s1) {
synchronized (s2) {
// 业务逻辑
}
}仅对真正需要同步的代码段加锁;
避免在同步块中执行耗时操作(如 I/O、sleep)。
利用 tryLock(timeout)(来自 java.util.concurrent.locks.ReentrantLock)代替无限等待;
若在指定时间内无法获得锁,则放弃并重试或回退。
Thread.join()当一个线程需等待另一个线程结束时,可使用带超时的 join(long millis):
thread.join(2000); // 最多等待 2 秒避免无限制等待,防止因被等待线程阻塞而导致连锁死锁。
死锁是多线程中因循环等待锁而造成的永久阻塞;
典型死锁需满足四个条件:互斥、持有并等待、不可抢占、循环等待;
Java 内置的 synchronized 机制若使用不当极易引发死锁;
可通过 jcmd 或 jstack 工具检测运行时死锁;
预防死锁的关键在于统一加锁顺序、减少锁粒度和引入超时机制。
在上述死锁示例中,如果将 test1 和 test2 方法改为静态同步方法,是否还会发生死锁?为什么?
除了调整加锁顺序,你还能想到哪些设计模式或并发工具(如 ReentrantLock、Semaphore)可用于避免死锁?
在实际项目中,如何通过代码审查或静态分析工具提前发现潜在的死锁风险?