死锁
两个线程互相等待对方持有的锁,谁也不放手。
一个经典场景
java
public class DeadlockDemo {
private static final Object resourceA = new Object();
private static final Object resourceB = new Object();
public static void main(String[] args) {
// 线程1:先拿 A,再拿 B
new Thread(() -> {
synchronized (resourceA) {
System.out.println("线程1: 拿到资源A");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (resourceB) { // 等 B
System.out.println("线程1: 拿到资源B");
}
}
}, "Thread-1").start();
// 线程2:先拿 B,再拿 A
new Thread(() -> {
synchronized (resourceB) {
System.out.println("线程2: 拿到资源B");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (resourceA) { // 等 A
System.out.println("线程2: 拿到资源A");
}
}
}, "Thread-2").start();
}
}运行结果:程序卡死。
线程1: 拿到资源A
线程2: 拿到资源B
(卡住不动了)死锁的四个必要条件
发生死锁必须同时满足以下四个条件:
| 条件 | 含义 |
|---|---|
| 互斥条件 | 资源只能被一个线程持有 |
| 持有并等待 | 线程持有资源,同时等待其他资源 |
| 不可抢占 | 资源不能被强制释放 |
| 循环等待 | 形成 A 等 B、B 等 A 的循环 |
破坏任一条件,死锁就不会发生。
如何排查死锁?
方法一:jstack 检测
bash
# 找到 Java 进程
jps -l
# 导出线程堆栈
jstack <pid>输出:
Found one Java-level deadlock:
=========================
"Thread-2":
waiting for monitor lock 0x00007f8a5400a000,
which is held by "Thread-1"
"Thread-1":
waiting for monitor lock 0x00007f8a54009000,
which is held by "Thread-2"
Java stack information for the threads listed above:
==================================================
"Thread-2":
at DeadlockDemo.lambda$main$1(DeadlockDemo.java:20)
- waiting to lock <0x00007f8a54009000>
- locked <0x00007f8a5400a000>
"Thread-1":
at DeadlockDemo.lambda$main$0(DeadlockDemo.java:10)
- waiting to lock <0x00007f8a5400a000>
- locked <0x00007f8a54009000>jstack 会自动检测并报告死锁。
方法二:JConsole / VisualVM
图形化工具查看线程状态。
方法三:代码检测
java
// 定时检测死锁
ThreadMXBean threadMX = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = threadMX.findDeadlockedThreads();
if (deadlockedThreads != null) {
for (long id : deadlockedThreads) {
ThreadInfo info = threadMX.getThreadInfo(id);
System.out.println("死锁线程: " + info.getThreadName());
}
}如何解决死锁?
方案一:固定加锁顺序
破坏循环等待条件。
java
public class FixedOrderSolution {
private static final Object resourceA = new Object();
private static final Object resourceB = new Object();
// 始终按固定顺序获取
public void method1() {
synchronized (resourceA) { // 始终先拿 A
System.out.println("method1: 拿到资源A");
synchronized (resourceB) {
System.out.println("method1: 拿到资源B");
}
}
}
public void method2() {
synchronized (resourceA) { // 同样先拿 A
System.out.println("method2: 拿到资源A");
synchronized (resourceB) {
System.out.println("method2: 拿到资源B");
}
}
}
}注意:按对象的「天然顺序」(如 ID、hashCode)排序更好:
java
public void transfer(Account from, Account to, int amount) {
// 按 ID 排序,保证顺序一致
Account first = from.id < to.id ? from : to;
Account second = from.id < to.id ? to : from;
synchronized (first) {
synchronized (second) {
// 转账
}
}
}方案二:tryLock 超时
破坏不可抢占条件。
java
public class TryLockSolution {
private static final ReentrantLock lockA = new ReentrantLock();
private static final ReentrantLock lockB = new ReentrantLock();
public void transfer() {
while (true) {
// 尝试获取 lockA,最多等 1 秒
if (lockA.tryLock(1, TimeUnit.SECONDS)) {
try {
// 尝试获取 lockB,最多等 1 秒
if (lockB.tryLock(1, TimeUnit.SECONDS)) {
try {
// 执行操作
System.out.println("转账成功");
return;
} finally {
lockB.unlock();
}
}
} finally {
lockA.unlock();
}
}
// 都没拿到,休息一下再重试
Thread.sleep(10);
}
}
}方案三:使用单个锁
破坏持有并等待条件。
java
public class SingleLockSolution {
private final Object globalLock = new Object();
private int resourceA = 0;
private int resourceB = 0;
// 两个操作都在同一个锁里
public void method1() {
synchronized (globalLock) {
resourceA++;
resourceB--;
}
}
public void method2() {
synchronized (globalLock) {
resourceA--;
resourceB++;
}
}
}缺点:并发性能降低。
方案四:LockTimeout + 回滚
java
public class TimeoutWithRollback {
private final ReentrantLock lock1 = new ReentrantLock();
private final ReentrantLock lock2 = new ReentrantLock();
public boolean transfer(Account from, Account to, int amount) {
try {
// 尝试获取两个锁
if (!lock1.tryLock(1, TimeUnit.SECONDS)) return false;
if (!lock2.tryLock(1, TimeUnit.SECONDS)) return false;
// 执行转账
if (from.balance >= amount) {
from.balance -= amount;
to.balance += amount;
return true;
}
return false;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
} finally {
if (lock2.isHeldByCurrentThread()) lock2.unlock();
if (lock1.isHeldByCurrentThread()) lock1.unlock();
}
}
}实战:银行转账
java
public class BankTransfer {
private final Map<String, Account> accounts = new HashMap<>();
private final ReentrantLock lock = new ReentrantLock();
public void transfer(String fromId, String toId, int amount) {
Account from = accounts.get(fromId);
Account to = accounts.get(toId);
// 按 ID 排序,保证加锁顺序一致
Account first = from.id.compareTo(to.id) < 0 ? from : to;
Account second = from.id.compareTo(to.id) < 0 ? to : from;
lock.lock();
try {
if (first.balance >= amount) {
first.balance -= amount;
second.balance += amount;
System.out.println("转账成功: " + fromId + " -> " + toId + " = " + amount);
}
} finally {
lock.unlock();
}
}
static class Account {
String id;
int balance;
Account(String id, int balance) {
this.id = id;
this.balance = balance;
}
}
}死锁 vs 活锁
| 死锁 | 活锁 | |
|---|---|---|
| 线程状态 | 阻塞 | 运行中 |
| 现象 | 都不动 | 都在动,但没进展 |
| 原因 | 互相等待 | 不断重试 |
活锁示例:
java
// 两个线程都在运行,但都在礼让对方
while (true) {
if (tryLock(resourceA)) {
if (tryLock(resourceB)) {
// 成功
return;
}
unlock(resourceA);
}
// 立即重试,导致一直冲突
}解决活锁:加入随机退避时间。
java
while (true) {
if (lockA.tryLock()) {
if (lockB.tryLock()) {
return;
}
lockA.unlock();
}
Thread.sleep(new Random().nextInt(100)); // 随机退避
}总结
死锁的四个条件(同时满足才发生):
- 互斥条件
- 持有并等待
- 不可抢占
- 循环等待
解决方案:
| 方案 | 破坏条件 | 适用场景 |
|---|---|---|
| 固定加锁顺序 | 循环等待 | 多锁必须同时获取 |
| tryLock 超时 | 不可抢占 | 复杂锁场景 |
| 单锁 | 持有并等待 | 简单场景 |
| 随机退避 | 活锁 | 重试冲突 |
最佳实践:
- 尽量避免多锁场景
- 多锁时用固定顺序
- 用 tryLock 防止永久等待
- 线上定期检测死锁
