在Java多线程编程场景中,死锁是多个线程互相持有对方等待的资源且不主动释放,最终导致所有相关线程都无法继续执行的阻塞问题,会直接造成程序部分功能不可用,甚至整个服务卡顿。理解死锁的成因并掌握对应的规避、检测、恢复方法,是开发高可用并发程序的必备能力。
死锁产生的必要条件
要有效避免死锁,首先需要明确死锁产生的四个必要条件,只要破坏其中任意一个条件,就可以避免死锁发生:
- 互斥条件:资源同一时间只能被一个线程占用,其他线程需要等待该线程释放后才能获取。
- 请求并保持条件:线程已经持有了至少一个资源,同时又提出了新的资源请求,而新资源被其他线程占用,此时该线程不会释放自己已持有的资源。
- 不可剥夺条件:线程已获得的资源,在未主动释放之前,不能被其他线程强行剥夺。
- 循环等待条件:存在一组线程,每个线程都在等待下一个线程持有的资源,形成环形等待链。
避免死锁的常用策略
1. 按顺序获取锁
破坏循环等待条件是最常用的避免死锁方式,所有线程都按照固定的顺序获取锁,就不会形成环形等待链。比如有两个资源锁A和锁B,约定所有线程都先获取锁A再获取锁B,就不会出现死锁。
以下是错误和正确的锁获取示例:
// 错误示例:不同线程获取锁顺序不一致,可能产生死锁
class WrongLockOrder {
private final Object lockA = new Object();
private final Object lockB = new Object();
// 线程1先获取lockA再获取lockB
public void method1() {
synchronized (lockA) {
System.out.println("线程1获取lockA");
synchronized (lockB) {
System.out.println("线程1获取lockB");
}
}
}
// 线程2先获取lockB再获取lockA,可能和线程1形成死锁
public void method2() {
synchronized (lockB) {
System.out.println("线程2获取lockB");
synchronized (lockA) {
System.out.println("线程2获取lockA");
}
}
}
}
// 正确示例:固定锁获取顺序,先A后B
class RightLockOrder {
private final Object lockA = new Object();
private final Object lockB = new Object();
public void method1() {
synchronized (lockA) {
System.out.println("线程1获取lockA");
synchronized (lockB) {
System.out.println("线程1获取lockB");
}
}
}
public void method2() {
// 同样先获取lockA再获取lockB,避免顺序不一致
synchronized (lockA) {
System.out.println("线程2获取lockA");
synchronized (lockB) {
System.out.println("线程2获取lockB");
}
}
}
}
2. 使用超时锁
Java的Lock接口提供了tryLock方法,支持设置获取锁的超时时间,在超时时间内没有获取到锁就主动放弃,同时释放自己已持有的资源,破坏请求并保持条件。这种方式可以避免线程无限等待。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class TimeoutLockDemo {
private final Lock lockA = new ReentrantLock();
private final Lock lockB = new ReentrantLock();
public void method1() {
boolean getLockA = false;
boolean getLockB = false;
try {
// 尝试在1秒内获取lockA
getLockA = lockA.tryLock(1, TimeUnit.SECONDS);
if (getLockA) {
System.out.println("线程1获取lockA成功");
// 尝试在1秒内获取lockB
getLockB = lockB.tryLock(1, TimeUnit.SECONDS);
if (getLockB) {
System.out.println("线程1获取lockB成功,执行逻辑");
} else {
System.out.println("线程1获取lockB超时,放弃执行");
}
} else {
System.out.println("线程1获取lockA超时,放弃执行");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 释放已获取的锁
if (getLockB) {
lockB.unlock();
}
if (getLockA) {
lockA.unlock();
}
}
}
public void method2() {
boolean getLockA = false;
boolean getLockB = false;
try {
getLockB = lockB.tryLock(1, TimeUnit.SECONDS);
if (getLockB) {
System.out.println("线程2获取lockB成功");
getLockA = lockA.tryLock(1, TimeUnit.SECONDS);
if (getLockA) {
System.out.println("线程2获取lockA成功,执行逻辑");
} else {
System.out.println("线程2获取lockA超时,放弃执行");
}
} else {
System.out.println("线程2获取lockB超时,放弃执行");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (getLockA) {
lockA.unlock();
}
if (getLockB) {
lockB.unlock();
}
}
}
}
3. 减少锁的持有时间
尽量缩短线程持有锁的时间,只在需要操作共享资源的代码段加锁,无关逻辑放到锁外部执行,这样可以减少其他线程等待锁的时间,降低死锁发生的概率。
死锁检测方法
如果无法完全避免死锁,就需要对死锁进行检测,Java提供了多种检测死锁的方式:
1. 使用jstack工具检测
jstack是JDK自带的线程栈分析工具,可以打印出Java进程中所有线程的栈信息,其中会明确标记出死锁相关的线程和资源信息。使用方法是在命令行执行jstack 进程ID,如果进程存在死锁,输出内容中会包含Found one Java-level deadlock的提示,同时会列出死锁的线程、持有的锁和等待的锁信息。
2. 使用ThreadMXBean编程检测
Java的ThreadMXBean接口提供了获取线程死锁信息的方法,可以在程序中主动检测死锁,以下是检测死锁的示例代码:
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;
import java.lang.management.ThreadInfo;
class DeadlockDetector {
public static void detectDeadlock() {
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
// 获取死锁的线程ID数组
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
if (deadlockedThreads == null) {
System.out.println("当前没有检测到死锁");
return;
}
System.out.println("检测到死锁,涉及线程ID:");
for (long threadId : deadlockedThreads) {
System.out.println(threadId);
}
// 获取死锁线程的详细信息
ThreadInfo[] threadInfos = threadMXBean.getThreadInfo(deadlockedThreads);
for (ThreadInfo info : threadInfos) {
System.out.println("线程名:" + info.getThreadName());
System.out.println("线程状态:" + info.getThreadState());
System.out.println("持有锁:" + info.getLockedMonitors());
System.out.println("等待锁:" + info.getLockName());
}
}
}
死锁恢复策略
检测到死锁之后,需要根据实际场景选择恢复策略:
- 线程终止恢复:终止死锁环中的一个或多个线程,释放它们持有的资源,让其他线程可以继续执行。这种方式适合那些被终止的线程任务可以重试或者不影响核心业务的场景。
- 资源抢占恢复:强行剥夺死锁线程持有的资源,分配给其他等待的线程。不过Java原生的锁机制不支持资源抢占,通常需要自己实现资源的管理逻辑,比如给资源加优先级,高优先级线程可以抢占低优先级线程的资源。
- 回滚恢复:让死锁的线程回滚到获取锁之前的状态,释放所有已持有的资源,然后重新尝试获取资源。这种方式适合事务性的操作场景,比如数据库事务的回滚逻辑。
总结
避免死锁的核心是从设计层面破坏死锁产生的必要条件,优先采用固定锁顺序、超时锁等方式预防死锁。对于无法完全避免的场景,要配合死锁检测机制及时发现问题,再根据业务场景选择合适的恢复策略,保障并发程序的稳定运行。