Java 死锁恢复(线程中断 + 资源释放)
下文笔者讲述java中死锁恢复的方法及示例分享,如下所示
线程回复方法
死锁恢复的前置条件
Java 中死锁的本质是
多个线程互相持有对方所需的锁,
且均处于无限阻塞状态(BLOCKED/WAITING)
线程无法主动感知死锁
也无法自行释放已持有的锁
基于线程中断 + 资源释放的恢复方案,核心前提是:
1.线程在等待锁(或其他资源)时,
需支持响应中断(避免线程忽略中断信号,无法触发后续资源释放)
2.需提前设计
锁资源的释放逻辑
(保线程收到中断信号后,能主动释放已持有的锁,打破死锁依赖链)
3.需先通过前文提到的 jstack、Arthas 等工具
精准定位死锁线程和锁关系
(明确需要中断哪些线程、释放哪些资源)
死锁恢复的方法汇总
Java 死锁恢复(线程中断 + 资源释放)的核心要点:
1. 核心逻辑:中断死锁线程 → 线程响应中断(抛出 `InterruptedException`)→
`finally` 块释放已持有的锁 → 打破死锁依赖;
2. 核心技术:依赖 `Lock` 接口的 `lockInterruptibly()`
(可中断锁)和 `tryLock()`(超时锁),`synchronized` 锁不支持该方案;
3. 关键保障:资源释放必须放在 `finally` 块,
遵循「反向获取顺序」释放锁;
4. 实践原则:优先通过「统一锁顺序、减少锁粒度」预防死锁,
线上死锁可通过 Arthas 快速中断恢复,无法恢复时优雅重启应用。
线程回复方法
方案1:基于可中断锁(`Lock` 接口)的恢复
`synchronized` 关键字修饰的锁是不可中断的
(线程等待 `synchronized` 锁时,无法通过中断唤醒),
而`java.util.concurrent.locks.Lock` 接口提供了可中断的锁获取方法(`lockInterruptibly()`)
是实现死锁恢复的核心技术。
核心步骤:
1.使用 `ReentrantLock` 等实现类替代 `synchronized`,作为锁资源;
2.线程通过 `lockInterruptibly()` 方法获取锁
(该方法会响应中断,若线程在等待锁时被中断,会抛出 `InterruptedException`);
3.捕获 `InterruptedException` 异常,
在异常处理块中释放已持有的其他锁资源;
4.定位死锁线程后,调用 `Thread.interrupt()` 发送中断信号,
触发线程响应并释放资源。
示例
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/
* 基于可中断锁的死锁恢复示例
*/
public class InterruptibleLockDeadLockRecovery {
// 定义两个可中断锁资源
private static final Lock LOCK_A = new ReentrantLock();
private static final Lock LOCK_B = new ReentrantLock();
// 线程1:尝试先获取LOCK_A,再获取LOCK_B
private static class Thread1 extends Thread {
@Override
public void run() {
boolean acquiredA = false;
boolean acquiredB = false;
try {
// 可中断方式获取锁A:响应中断信号
LOCK_A.lockInterruptibly();
acquiredA = true;
System.out.println(Thread.currentThread().getName() + " 已获取锁A,等待获取锁B");
// 模拟业务延迟,触发死锁
Thread.sleep(1000);
// 可中断方式获取锁B:响应中断信号
LOCK_B.lockInterruptibly();
acquiredB = true;
System.out.println(Thread.currentThread().getName() + " 已获取锁B,执行业务逻辑");
} catch (InterruptedException e) {
// 响应中断:打印中断信息,准备释放已持有的锁
System.out.println(Thread.currentThread().getName() + " 收到中断信号,即将释放已持有的锁");
Thread.currentThread().interrupt(); // 保留中断标志位(可选)
} finally {
// 资源释放:仅释放已成功获取的锁,避免非法释放异常
if (acquiredB) {
LOCK_B.unlock();
System.out.println(Thread.currentThread().getName() + " 释放锁B");
}
if (acquiredA) {
LOCK_A.unlock();
System.out.println(Thread.currentThread().getName() + " 释放锁A");
}
}
}
}
// 线程2:尝试先获取LOCK_B,再获取LOCK_A(与线程1锁顺序相反,触发死锁)
private static class Thread2 extends Thread {
@Override
public void run() {
boolean acquiredB = false;
boolean acquiredA = false;
try {
// 可中断方式获取锁B
LOCK_B.lockInterruptibly();
acquiredB = true;
System.out.println(Thread.currentThread().getName() + " 已获取锁B,等待获取锁A");
// 模拟业务延迟,触发死锁
Thread.sleep(1000);
// 可中断方式获取锁A
LOCK_A.lockInterruptibly();
acquiredA = true;
System.out.println(Thread.currentThread().getName() + " 已获取锁A,执行业务逻辑");
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " 收到中断信号,即将释放已持有的锁");
Thread.currentThread().interrupt();
} finally {
// 按反向顺序释放已获取的锁
if (acquiredA) {
LOCK_A.unlock();
System.out.println(Thread.currentThread().getName() + " 释放锁A");
}
if (acquiredB) {
LOCK_B.unlock();
System.out.println(Thread.currentThread().getName() + " 释放锁B");
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread1();
Thread t2 = new Thread2();
t1.setName("死锁线程-1");
t2.setName("死锁线程-2");
// 启动线程,触发死锁
t1.start();
t2.start();
// 等待3秒,确认死锁形成
Thread.sleep(3000);
System.out.println("===== 检测到死锁,开始执行中断恢复 =====");
// 中断其中一个死锁线程(打破死锁依赖)
t1.interrupt();
// 等待线程执行完毕,观察资源释放情况
t1.join();
t2.join();
System.out.println("===== 死锁恢复完成,所有锁资源已释放 =====");
}
}
代码说明
运行结果说明:
1.线程1和线程2启动后,因锁获取顺序相反形成死锁;
2.主线程3秒后发送中断信号给线程1;
3.线程1响应中断,进入 `InterruptedException` 异常块,
最终在 `finally` 中释放已持有的锁A;
4.线程2获取到锁A,执行完业务逻辑后释放锁A和锁B,死锁被打破。
方案2:基于超时锁(`tryLock()`)的恢复(无需主动中断)
`Lock.tryLock(long time, TimeUnit unit)` 方法支持设置锁获取超时时间,
若超时未获取到锁,会返回 `false`,线程可主动释放已持有的锁,
避免进入死锁状态(本质是「预防+恢复」结合,提前规避死锁,也可用于已发生死锁的场景)。
核心步骤:
1.使用 `tryLock()` 方法设置超时时间,尝试获取锁;
2.若超时未获取到锁,直接返回,在 `finally` 中释放已持有的其他锁;
3.可选:配合线程中断,增强灵活性。
代码示例
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/
* 基于超时锁的死锁恢复(规避)示例
*/
public class TimeoutLockDeadLockRecovery {
private static final Lock LOCK_A = new ReentrantLock();
private static final Lock LOCK_B = new ReentrantLock();
private static class BusinessThread extends Thread {
private final String threadName;
private final Lock firstLock;
private final Lock secondLock;
public BusinessThread(String threadName, Lock firstLock, Lock secondLock) {
this.threadName = threadName;
this.firstLock = firstLock;
this.secondLock = secondLock;
}
@Override
public void run() {
boolean acquiredFirst = false;
boolean acquiredSecond = false;
try {
// 尝试获取第一个锁(立即获取)
acquiredFirst = firstLock.tryLock(0, TimeUnit.MILLISECONDS);
if (!acquiredFirst) {
System.out.println(threadName + " 未获取到第一个锁,直接退出");
return;
}
System.out.println(threadName + " 已获取第一个锁,等待获取第二个锁");
// 尝试获取第二个锁,设置3秒超时
acquiredSecond = secondLock.tryLock(3, TimeUnit.SECONDS);
if (!acquiredSecond) {
System.out.println(threadName + " 获取第二个锁超时,即将释放第一个锁");
return;
}
System.out.println(threadName + " 已获取第二个锁,执行业务逻辑");
} catch (InterruptedException e) {
System.out.println(threadName + " 被中断,即将释放已持有的锁");
Thread.currentThread().interrupt();
} finally {
// 释放已获取的锁
if (acquiredSecond) {
secondLock.unlock();
System.out.println(threadName + " 释放第二个锁");
}
if (acquiredFirst) {
firstLock.unlock();
System.out.println(threadName + " 释放第一个锁");
}
}
}
}
public static void main(String[] args) throws InterruptedException {
// 线程1:先获取A,再获取B
BusinessThread t1 = new BusinessThread("业务线程-1", LOCK_A, LOCK_B);
// 线程2:先获取B,再获取A(易死锁)
BusinessThread t2 = new BusinessThread("业务线程-2", LOCK_B, LOCK_A);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("===== 执行完成,无死锁残留 =====");
}
}
代码运行结果说明
1.线程1和线程2分别获取锁A和锁B后,
互相等待对方的锁;
2.3秒后,其中一个线程获取第二个锁超时,
返回并释放已持有的锁;
3.另一个线程成功获取第二个锁,执行完业务后释放所有锁,
避免死锁持续。
方案3:基于线程状态检测的主动中断恢复(线上实战)
针对已发生死锁的线上应用,需先通过工具定位死锁线程, 再通过线程 ID 找到对应的 `Thread` 对象(或通过线程池管理线程), 主动发送中断信号实现恢复。 核心步骤: 1. 通过 `jstack`/`Arthas` 定位死锁线程的 ID(10进制)和线程名称; 2. 在应用中维护线程管理容器(如 `Map<Long, Thread>`),或通过线程池获取活跃线程; 3. 根据线程 ID/名称找到目标线程,调用 `interrupt()` 中断; 4. 线程响应中断后,释放已持有的锁资源
代码示例
(线程管理+主动中断)
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/
* 基于线程管理+主动中断的线上死锁恢复示例
*/
public class ThreadManagerDeadLockRecovery {
// 线程管理容器:存储线程ID与Thread对象的映射
private static final Map<Long, Thread> THREAD_MAP = new HashMap<>();
private static final Lock LOCK_A = new ReentrantLock();
private static final Lock LOCK_B = new ReentrantLock();
// 注册线程到管理容器
private static void registerThread(Thread thread) {
THREAD_MAP.put(thread.getId(), thread);
}
// 根据线程ID获取线程
private static Thread getThreadById(long threadId) {
return THREAD_MAP.get(threadId);
}
// 死锁线程任务
private static class DeadLockTask implements Runnable {
private final Lock firstLock;
private final Lock secondLock;
public DeadLockTask(Lock firstLock, Lock secondLock) {
this.firstLock = firstLock;
this.secondLock = secondLock;
}
@Override
public void run() {
Thread currentThread = Thread.currentThread();
boolean acquiredFirst = false;
boolean acquiredSecond = false;
try {
// 可中断方式获取锁
firstLock.lockInterruptibly();
acquiredFirst = true;
System.out.println(currentThread.getName() + " 已获取第一个锁,线程ID:" + currentThread.getId());
Thread.sleep(1000); // 模拟业务延迟,触发死锁
secondLock.lockInterruptibly();
acquiredSecond = true;
System.out.println(currentThread.getName() + " 已获取第二个锁,执行业务逻辑");
} catch (InterruptedException e) {
System.out.println(currentThread.getName() + " 收到中断信号,线程ID:" + currentThread.getId());
Thread.currentThread().interrupt();
} finally {
// 释放锁资源
if (acquiredSecond) {
secondLock.unlock();
System.out.println(currentThread.getName() + " 释放第二个锁");
}
if (acquiredFirst) {
firstLock.unlock();
System.out.println(currentThread.getName() + " 释放第一个锁");
}
// 从管理容器中移除线程
THREAD_MAP.remove(currentThread.getId());
}
}
}
public static void main(String[] args) throws InterruptedException {
// 创建并注册线程1
Thread t1 = new Thread(new DeadLockTask(LOCK_A, LOCK_B), "死锁线程-1");
registerThread(t1);
// 创建并注册线程2
Thread t2 = new Thread(new DeadLockTask(LOCK_B, LOCK_A), "死锁线程-2");
registerThread(t2);
// 启动线程,触发死锁
t1.start();
t2.start();
// 等待3秒,确认死锁形成
Thread.sleep(3000);
System.out.println("===== 线上检测到死锁,开始定位并中断 =====");
// 步骤1:通过jstack/Arthas获取死锁线程ID(此处模拟,实际需工具定位)
long deadLockThreadId1 = t1.getId();
System.out.println("定位到死锁线程ID:" + deadLockThreadId1);
// 步骤2:根据线程ID获取线程,发送中断信号
Thread targetThread = getThreadById(deadLockThreadId1);
if (targetThread != null) {
targetThread.interrupt();
System.out.println("已向线程ID:" + deadLockThreadId1 + " 发送中断信号");
}
// 等待线程执行完毕
t1.join();
t2.join();
System.out.println("===== 线上死锁恢复完成 =====");
}
}
注意事项
1. `synchronized` 锁无法通过中断实现恢复
`synchronized` 是不可中断的同步机制,线程在等待 `synchronized` 锁时,
会忽略 `interrupt()` 中断信号,无法触发异常或退出等待状态。
因此,死锁恢复方案仅适用于 `java.util.concurrent.locks` 包下的可中断锁。
2. 资源释放必须放在 `finally` 块中
无论线程是否被中断、是否发生异常,`finally` 块都会执行,
确保已获取的锁资源能被释放,避免锁泄漏(若锁未释放,
会导致其他线程无法获取锁,引发新的阻塞问题)。
3. 锁释放需遵循「反向获取顺序」
获取锁时按「锁A→锁B」的顺序,释放时需按「锁B→锁A」的反向顺序,
避免因释放顺序错误导致的异常或资源混乱。
4. 中断标志位的处理
线程响应中断后,`InterruptedException` 会清除中断标志位,
若需要后续逻辑感知中断状态,
可在异常块中调用 `Thread.currentThread().interrupt()` 重新设置标志位。
5. 避免盲目中断所有死锁线程
通常只需中断其中一个死锁线程,即可打破死锁依赖链;
若中断所有线程,可能导致业务逻辑中断、数据不一致等问题,
需结合业务场景选择中断目标。
四、死锁恢复的局限性与最佳实践
1. 局限性
- 恢复具有滞后性:需先检测到死锁,才能执行恢复操作,期间应用已处于异常状态;
- 业务影响风险:中断线程可能导致业务逻辑中断、数据不完整(如数据库事务未提交、缓存不一致);
- 仅适用于可中断锁:对 `synchronized` 锁引发的死锁,无法通过中断恢复,只能重启应用;
- 复杂场景难以覆盖:分布式死锁、多资源交叉死锁等场景,单纯的线程中断无法实现完整恢复。
2. 最佳实践(优先预防,其次恢复)
死锁恢复是「事后补救」手段,更高效的方式是提前预防死锁,结合按需恢复:
1. 统一锁获取顺序:所有线程按相同的顺序获取锁(如按锁对象的哈希值排序),从根源避免死锁;
2. 使用可中断/超时锁:优先使用 `lockInterruptibly()` 和 `tryLock()`,提前规避死锁;
3. 减少锁粒度:拆分全局锁为局部锁,避免不必要的锁竞争;
4. 线上监控告警:通过 Arthas、Prometheus + Grafana 实时监控线程状态,提前预警死锁;
5. 紧急恢复预案:对 `synchronized` 锁引发的死锁,或复杂分布式死锁,最可靠的方式是优雅重启应用(需确保应用具备故障转移、数据恢复能力)。
版权声明
本文仅代表作者观点,不代表本站立场。
本文系作者授权发表,未经许可,不得转载。


