引言
在多线程并发编程的世界中,死锁(Deadlock) 是最令人头疼的问题之一。它就像交通中的" deadlock "路口,所有车辆都互相等待对方让路,结果导致整个系统停滞不前。在 Java 应用中,死锁可能导致服务无响应、线程池耗尽,甚至引发严重的生产事故。
本文将深入探讨 Java 死锁的成因、检测手段、工具使用以及预防策略,并配合详细的代码示例和避坑指南,帮助你彻底掌握这一并发编程的核心难点。
一、什么是死锁?
1.1 定义
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象。若无外力作用,它们都将无法推进下去。
1.2 死锁产生的四个必要条件(Coffman 条件)
要形成死锁,必须同时满足以下四个条件:
- 互斥条件(Mutual Exclusion):资源是独占的,同一时刻只能被一个线程占用。
- 请求与保持条件(Hold and Wait):线程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他线程占有。
- 不剥夺条件(No Preemption):线程已获得的资源,在未使用完之前,不能强行剥夺。
- 循环等待条件(Circular Wait):若干线程之间形成一种头尾相接的循环等待资源关系。
关键点:只要破坏其中任何一个条件,死锁就不会发生。
二、Java 死锁形成的原因与代码复现
2.1 经典场景:交叉锁定
最常见的死锁场景是两个线程以不同的顺序获取相同的锁。
代码示例:制造死锁
java
public class DeadlockExample {
// 定义两个锁对象
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
// 线程 A:先拿 lock1,再拿 lock2
Thread threadA = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread A: Holding lock1...");
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("Thread A: Waiting for lock2...");
synchronized (lock2) {
System.out.println("Thread A: Holding lock1 and lock2...");
}
}
});
// 线程 B:先拿 lock2,再拿 lock1
Thread threadB = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread B: Holding lock2...");
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("Thread B: Waiting for lock1...");
synchronized (lock1) {
System.out.println("Thread B: Holding lock1 and lock2...");
}
}
});
threadA.start();
threadB.start();
// 等待线程结束(实际上永远不会结束)
try {
threadA.join();
threadB.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Program finished."); // 这行代码永远不会执行
}
}
运行结果分析:
- Thread A 持有
lock1,等待lock2。 - Thread B 持有
lock2,等待lock1。 - 双方都在等待对方释放资源,形成闭环,程序卡死。
三、如何检测和排查死锁?
当生产环境出现死锁时,我们需要快速定位问题。Java 提供了多种工具和命令。
3.1 使用 JDK 自带工具:jstack
jstack 是 JDK 自带的命令行工具,可以打印 Java 进程的线程堆栈信息,并能自动检测死锁。
步骤:
-
找到 Java 进程 ID (PID):
bashjps -l # 或者 ps -ef | grep java -
执行 jstack:
bashjstack <PID>
输出示例 : 在 jstack 的输出末尾,如果检测到死锁,会明确显示:
text
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00007f8b8c001230 (object 0x000000076ab5d8e0, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x00007f8b8c001240 (object 0x000000076ab5d8f0, a java.lang.Object),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
...
3.2 使用 JConsole / VisualVM
图形化工具更直观:
- JConsole:启动后连接进程 -> 点击 "Threads" 标签 -> 点击 "Detect Deadlock" 按钮。
- VisualVM:功能更强大,可以查看线程状态、CPU 占用,并直接高亮显示死锁线程。
3.3 编程方式检测:ThreadMXBean
如果你需要在代码中自动检测死锁(例如健康检查接口),可以使用 java.lang.management.ThreadMXBean。
java
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
public class DeadlockDetector {
public static void checkForDeadlock() {
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
if (deadlockedThreads != null && deadlockedThreads.length > 0) {
System.out.println("Detected " + deadlockedThreads.length + " deadlocked threads:");
ThreadInfo[] threadInfos = threadMXBean.getThreadInfo(deadlockedThreads);
for (ThreadInfo info : threadInfos) {
System.out.println(info.toString());
}
} else {
System.out.println("No deadlocks found.");
}
}
public static void main(String[] args) {
// 启动死锁线程(参考上面的 DeadlockExample)
// ... 启动代码 ...
// 模拟等待一段时间后检测
try { Thread.sleep(2000); } catch (InterruptedException e) {}
checkForDeadlock();
}
}
四、避免死锁的措施与最佳实践
既然死锁危害巨大,我们该如何避免?核心思路是破坏死锁产生的四个必要条件。
4.1 策略一:固定锁的获取顺序(破坏循环等待)
这是最有效的方法。确保所有线程都以相同的顺序获取锁。
修正后的代码:
java
public class FixedOrderLocking {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
// 辅助方法:总是先获取 hashCode 小的锁,再获取大的
private static void acquireLocks(Object first, Object second) {
int hashFirst = System.identityHashCode(first);
int hashSecond = System.identityHashCode(second);
if (hashFirst < hashSecond) {
synchronized (first) {
synchronized (second) {
doWork();
}
}
} else {
synchronized (second) {
synchronized (first) {
doWork();
}
}
}
}
private static void doWork() {
System.out.println("Working with both locks safely: " + Thread.currentThread().getName());
}
public static void main(String[] args) {
Runnable task = () -> acquireLocks(lock1, lock2);
new Thread(task).start();
new Thread(task).start();
// 无论多少个线程,都不会死锁
}
}
4.2 策略二:使用定时锁尝试(破坏请求与保持)
使用 ReentrantLock 的 tryLock(timeout, unit) 方法。如果在规定时间内无法获取锁,则放弃当前操作,稍后重试或回滚。
java
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;
public class TryLockExample {
private static final ReentrantLock lock1 = new ReentrantLock();
private static final ReentrantLock lock2 = new ReentrantLock();
public static void transferMoney(Account from, Account to, int amount) {
boolean gotLock1 = false;
boolean gotLock2 = false;
while (true) {
try {
// 尝试获取第一个锁,超时 1 秒
gotLock1 = lock1.tryLock(1, TimeUnit.SECONDS);
// 尝试获取第二个锁,超时 1 秒
gotLock2 = lock2.tryLock(1, TimeUnit.SECONDS);
if (gotLock1 && gotLock2) {
// 成功获取两个锁,执行转账
System.out.println("Transferring " + amount + " from " + from.id + " to " + to.id);
return;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
} finally {
// 如果只获取了一个锁,必须释放它,防止持有资源等待
if (gotLock1) lock1.unlock();
if (gotLock2) lock2.unlock();
}
// 没获取到所有锁,随机等待一下再重试,避免活锁
try {
Thread.sleep((long) (Math.random() * 100));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}
static class Account {
int id;
Account(int id) { this.id = id; }
}
}
4.3 策略三:减小锁的粒度
尽量缩小同步代码块的范围,只保护真正的共享数据,而不是整个方法或大段逻辑。锁持有的时间越短,发生冲突的概率越低。
4.4 策略四:使用并发工具类代替手动锁
Java java.util.concurrent 包提供了许多高级工具,它们内部已经处理好了死锁问题:
ConcurrentHashMap:替代Hashtable或Collections.synchronizedMap。AtomicInteger,AtomicReference:利用 CAS 操作实现无锁线程安全。ExecutorService:管理线程池,避免手动创建大量线程。Semaphore,CountDownLatch,CyclicBarrier:协调线程执行。
4.5 策略五:避免嵌套锁
如果业务逻辑允许,尽量避免在一个同步块中调用另一个需要锁的方法。如果必须调用,确保被调用的方法不会去获取其他锁,或者遵循全局锁顺序。
五、Java 死锁避开指南(Checklist)
在代码 Review 或设计阶段,请对照以下清单进行检查:
| 序号 | 检查项 | 说明 |
|---|---|---|
| 1 | 锁顺序一致性 | 所有线程是否按照相同的全局顺序获取多个锁?(如:始终按 ID 排序后加锁) |
| 2 | 锁超时机制 | 是否使用了 tryLock 并设置了超时时间?是否有重试或降级逻辑? |
| 3 | 锁粒度 | 同步块是否足够小?是否包含了不必要的耗时操作(如 IO、网络请求)? |
| 4 | 嵌套锁风险 | 是否存在同步方法调用另一个同步方法的情况?是否可能形成环路? |
| 5 | 资源释放 | 是否在 finally 块中确保解锁?异常发生时锁是否会泄露? |
| 6 | 工具替代 | 是否可以用 Concurrent 包的工具类(如 ConcurrentHashMap)替代手动 synchronized? |
| 7 | 动态检测 | 关键系统是否集成了基于 ThreadMXBean 的死锁定期检测报警? |
| 8 | 文档规范 | 团队的开发规范中是否明确了锁的使用原则和顺序约定? |
六、总结
死锁是并发编程中的"隐形杀手",但只要理解其形成的四个必要条件,并采取针对性的预防措施,完全可以避免。
核心口诀:
顺序要统一,超时要设置,粒度要精细,工具优先选。
在实际开发中:
- 优先使用 JUC 包下的高层并发工具。
- 必须使用锁时,严格遵守"固定顺序"原则。
- 对于复杂的多锁场景,采用
tryLock超时机制作为兜底。 - 善用
jstack和ThreadMXBean进行监控和排查。
通过本文的代码示例和指南,希望你在面对多线程挑战时能更加从容,写出既高效又安全的 Java 代码。
作者 :架构师Beata
日期 :2026年3月8日
声明 :本文基于网络文档整理,如有疏漏,欢迎指正。转载请注明出处。
互动:如有任何问题?欢迎在评论区分享