恐怖解谜密室逃脱
109.73M · 2026-03-09
在多线程并发编程的世界中,死锁(Deadlock) 是最令人头疼的问题之一。它就像交通中的“ deadlock ”路口,所有车辆都互相等待对方让路,结果导致整个系统停滞不前。在 Java 应用中,死锁可能导致服务无响应、线程池耗尽,甚至引发严重的生产事故。
本文将深入探讨 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."); // 这行代码永远不会执行
}
}
运行结果分析:
lock1,等待 lock2。lock2,等待 lock1。当生产环境出现死锁时,我们需要快速定位问题。Java 提供了多种工具和命令。
jstack 是 JDK 自带的命令行工具,可以打印 Java 进程的线程堆栈信息,并能自动检测死锁。
步骤:
jps -l
# 或者
ps -ef | grep java
jstack <PID>
输出示例:
在 jstack 的输出末尾,如果检测到死锁,会明确显示:
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:
===================================================
...
图形化工具更直观:
如果你需要在代码中自动检测死锁(例如健康检查接口),可以使用 java.lang.management.ThreadMXBean。
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();
}
}
既然死锁危害巨大,我们该如何避免?核心思路是破坏死锁产生的四个必要条件。
这是最有效的方法。确保所有线程都以相同的顺序获取锁。
修正后的代码:
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();
// 无论多少个线程,都不会死锁
}
}
使用 ReentrantLock 的 tryLock(timeout, unit) 方法。如果在规定时间内无法获取锁,则放弃当前操作,稍后重试或回滚。
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; }
}
}
尽量缩小同步代码块的范围,只保护真正的共享数据,而不是整个方法或大段逻辑。锁持有的时间越短,发生冲突的概率越低。
Java java.util.concurrent 包提供了许多高级工具,它们内部已经处理好了死锁问题:
ConcurrentHashMap:替代 Hashtable 或 Collections.synchronizedMap。AtomicInteger, AtomicReference:利用 CAS 操作实现无锁线程安全。ExecutorService:管理线程池,避免手动创建大量线程。Semaphore, CountDownLatch, CyclicBarrier:协调线程执行。如果业务逻辑允许,尽量避免在一个同步块中调用另一个需要锁的方法。如果必须调用,确保被调用的方法不会去获取其他锁,或者遵循全局锁顺序。
在代码 Review 或设计阶段,请对照以下清单进行检查:
| 序号 | 检查项 | 说明 |
|---|---|---|
| 1 | 锁顺序一致性 | 所有线程是否按照相同的全局顺序获取多个锁?(如:始终按 ID 排序后加锁) |
| 2 | 锁超时机制 | 是否使用了 tryLock 并设置了超时时间?是否有重试或降级逻辑? |
| 3 | 锁粒度 | 同步块是否足够小?是否包含了不必要的耗时操作(如 IO、网络请求)? |
| 4 | 嵌套锁风险 | 是否存在同步方法调用另一个同步方法的情况?是否可能形成环路? |
| 5 | 资源释放 | 是否在 finally 块中确保解锁?异常发生时锁是否会泄露? |
| 6 | 工具替代 | 是否可以用 Concurrent 包的工具类(如 ConcurrentHashMap)替代手动 synchronized? |
| 7 | 动态检测 | 关键系统是否集成了基于 ThreadMXBean 的死锁定期检测报警? |
| 8 | 文档规范 | 团队的开发规范中是否明确了锁的使用原则和顺序约定? |
死锁是并发编程中的“隐形杀手”,但只要理解其形成的四个必要条件,并采取针对性的预防措施,完全可以避免。
核心口诀:
在实际开发中:
tryLock 超时机制作为兜底。jstack 和 ThreadMXBean 进行监控和排查。通过本文的代码示例和指南,希望你在面对多线程挑战时能更加从容,写出既高效又安全的 Java 代码。