Java线程死锁、活锁检测与解决
Java线程死锁由互斥、不可抢占、请求保持和循环等待四个条件引发,导致线程永久阻塞(如线程A持锁1等锁2,线程B持锁2等锁1),可通过统一锁获取顺序、tryLock超时机制和减少锁粒度预防;活锁则因线程过度协调(如双方不断重试释放锁)导致持续忙碌但系统无进展,需通过随机等待时间或指数退避算法解决。死锁检测依赖jstack(输出"DEADLOCK")或VisualVM可视化工具,活锁需分析日志和监控CPU高占用却无有效进展的特征。预防核心在于设计阶段统一锁顺序、避免嵌套锁、设置合理超时,运行时实施监控(如Arthas线程分析)和合理重试策略,确保高并发系统(如电商订单、实时交易)的稳定性和响应性。
一、死锁(Deadlock)
1. 死锁概念与产生条件
死锁是指两个或多个线程互相持有对方所需要的资源,都在等待对方执行完毕才能继续往下执行的状态,导致所有线程都陷入无限等待中。
死锁的四个必要条件(必须同时满足才会发生死锁):
- 互斥使用:资源被一个线程使用(占有)时,别的线程不能使用
- 不可抢占:资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放
- 请求和保持:当资源请求者在请求其他资源的同时保持对原有资源的占有
- 循环等待:存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源,形成等待环路
2. 死锁的典型场景与代码示例
错误示例(导致死锁)
public class DeadlockExample {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public void thread1Task() {
synchronized (lock1) {
System.out.println("Thread1: Holding lock1, waiting for lock2");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("Thread1: Holding lock1 and lock2");
}
}
}
public void thread2Task() {
synchronized (lock2) {
System.out.println("Thread2: Holding lock2, waiting for lock1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("Thread2: Holding lock2 and lock1");
}
}
}
public static void main(String[] args) {
DeadlockExample example = new DeadlockExample();
new Thread(example::thread1Task).start();
new Thread(example::thread2Task).start();
}
}
运行结果:程序将永久阻塞,无法继续执行。
3. 死锁检测方法
3.1 命令行方式
# 1. 查看Java进程ID
jps
# 2. 查看特定进程的线程状态
jstack <进程ID>
在输出中,如果出现"DEADLOCK"字样,即表示存在死锁。
3.2 可视化工具
-
VisualVM:
- 启动VisualVM并连接目标JVM
- 点击"线程"选项卡
- 在右上角过滤器中选择"检测死锁"功能
- 查看自动检测到的死锁线程组
- VisualVM会以图形方式展示线程之间的锁依赖关系
-
Java Mission Control (JMC):
- 提供"锁竞争分析"功能,可生成火焰图,直观显示哪些锁被频繁争用及等待时间分布
4. 死锁解决方法
4.1 统一锁获取顺序(推荐方法)
破坏"循环等待"条件,确保所有线程按照相同顺序获取锁。
public class DeadlockPrevention {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
// 所有线程按lock1 -> lock2的顺序获取锁
public void task() {
synchronized (lock1) {
System.out.println("Thread: Holding lock1, waiting for lock2");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("Thread: Holding lock1 and lock2");
}
}
}
public static void main(String[] args) {
DeadlockPrevention example = new DeadlockPrevention();
new Thread(example::task).start();
new Thread(example::task).start();
}
}
适用场景:大多数需要获取多个锁的业务场景,特别是订单处理、库存管理等需要保证操作原子性的系统。
4.2 使用超时获取锁(tryLock)
破坏"持有并等待"条件,避免线程无限等待。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;
public class TryLockExample {
private static final Lock lock1 = new ReentrantLock();
private static final Lock lock2 = new ReentrantLock();
public void task() {
boolean hasLock1 = false;
boolean hasLock2 = false;
try {
// 尝试获取lock1,超时1秒
hasLock1 = lock1.tryLock(1, TimeUnit.SECONDS);
if (hasLock1) {
System.out.println("获取lock1成功,尝试获取lock2");
// 尝试获取lock2,超时1秒
hasLock2 = lock2.tryLock(1, TimeUnit.SECONDS);
if (hasLock2) {
System.out.println("获取lock2成功,执行任务");
} else {
System.out.println("获取lock2超时,释放lock1");
}
} else {
System.out.println("获取lock1超时");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 释放已获取的锁
if (hasLock2) lock2.unlock();
if (hasLock1) lock1.unlock();
}
}
public static void main(String[] args) {
TryLockExample example = new TryLockExample();
new Thread(example::task).start();
new Thread(example::task).start();
}
}
适用场景:对响应时间有要求的系统,如Web服务、实时交易系统。
4.3 减少锁粒度
将大锁拆分为多个小锁,降低锁竞争。
public class ReducedLockGranularity {
private final Object orderLock = new Object();
private final Object inventoryLock = new Object();
public void createOrder(String userId, String productId) {
synchronized (orderLock) {
// 创建订单
}
synchronized (inventoryLock) {
// 更新库存
}
}
public void cancelOrder(String orderId) {
synchronized (orderLock) {
// 取消订单
}
synchronized (inventoryLock) {
// 恢复库存
}
}
}
适用场景:高并发系统,如电商网站的订单和库存处理。
5. 死锁的预防最佳实践
- 统一锁顺序:设计时确保所有线程按相同顺序获取锁
- 设置锁超时:使用ReentrantLock的tryLock方法
- 避免嵌套锁:尽量将多个锁操作拆分为独立的步骤
- 减少锁持有时间:在锁内不要进行耗时操作
- 使用更细粒度的锁:将大锁拆分为多个小锁
二、活锁(Livelock)
1. 活锁概念
活锁是指多个线程都在执行各自的操作,但由于彼此间的"协调不当",导致系统整体无法取得进展的状态。与死锁不同,活锁中的线程仍在"忙碌"工作,但整个系统却无法向前推进。
活锁的典型特征:
- 线程持续活动:CPU占用可能很高
- 无实际进展:系统状态没有实质性变化
- 无永久阻塞:线程没有被挂起或等待
- 响应式行为:线程之间相互响应对方的动作
2. 活锁的典型场景与代码示例
错误示例(导致活锁)
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LivelockDemo {
private static final Lock lock1 = new ReentrantLock();
private static final Lock lock2 = new ReentrantLock();
public static void main(String[] args) {
Thread threadA = new Thread(() -> {
while (true) {
if (lock1.tryLock()) {
System.out.println("ThreadA: 获得lock1");
try {
Thread.sleep(100); // 模拟工作
if (lock2.tryLock()) {
try {
System.out.println("ThreadA: 获得lock2 - 完成任务");
return; // 成功退出
} finally {
lock2.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock1.unlock();
}
}
System.out.println("ThreadA: 重试中...");
try {
Thread.sleep(50); // 避免CPU过载
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread threadB = new Thread(() -> {
while (true) {
if (lock2.tryLock()) {
System.out.println("ThreadB: 获得lock2");
try {
Thread.sleep(100); // 模拟工作
if (lock1.tryLock()) {
try {
System.out.println("ThreadB: 获得lock1 - 完成任务");
return; // 成功退出
} finally {
lock1.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock2.unlock();
}
}
System.out.println("ThreadB: 重试中...");
try {
Thread.sleep(50); // 避免CPU过载
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
threadA.start();
threadB.start();
}
}
运行结果:两个线程不断重试,但永远无法同时获取两把锁,系统无实际进展。
3. 活锁的检测方法
活锁检测相对困难,因为线程仍在运行。一般通过以下方式检测:
- 系统监控:观察系统CPU使用率高但无有效工作进展
- 日志分析:分析线程日志,发现线程不断重试但无成功
- 线程状态分析:使用jstack查看线程状态,确认线程处于RUNNABLE状态但无实际进展
4. 活锁解决方法
4.1 随机等待时间
在重试前加入随机等待时间,避免线程同时重试。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.ThreadLocalRandom;
public class LivelockFixed {
private static final Lock lock1 = new ReentrantLock();
private static final Lock lock2 = new ReentrantLock();
public static void main(String[] args) {
Thread threadA = new Thread(() -> {
while (true) {
if (lock1.tryLock()) {
System.out.println("ThreadA: 获得lock1");
try {
Thread.sleep(100); // 模拟工作
if (lock2.tryLock()) {
try {
System.out.println("ThreadA: 获得lock2 - 完成任务");
return; // 成功退出
} finally {
lock2.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock1.unlock();
}
}
System.out.println("ThreadA: 重试中...");
// 加入随机等待时间,避免与ThreadB同时重试
try {
Thread.sleep(ThreadLocalRandom.current().nextInt(50, 150));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread threadB = new Thread(() -> {
while (true) {
if (lock2.tryLock()) {
System.out.println("ThreadB: 获得lock2");
try {
Thread.sleep(100); // 模拟工作
if (lock1.tryLock()) {
try {
System.out.println("ThreadB: 获得lock1 - 完成任务");
return; // 成功退出
} finally {
lock1.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock2.unlock();
}
}
System.out.println("ThreadB: 重试中...");
// 加入随机等待时间,避免与ThreadA同时重试
try {
Thread.sleep(ThreadLocalRandom.current().nextInt(50, 150));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
threadA.start();
threadB.start();
}
}
适用场景:需要避免线程同时重试的场景,如网络请求重试、分布式系统资源竞争。
4.2 指数退避算法
在重试前等待更长时间,随着重试次数增加等待时间指数增长。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ExponentialBackoff {
private static final Lock lock1 = new ReentrantLock();
private static final Lock lock2 = new ReentrantLock();
public void task() {
int retryCount = 0;
while (true) {
if (lock1.tryLock()) {
try {
if (lock2.tryLock()) {
try {
System.out.println("成功获取锁,执行任务");
return;
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
// 指数退避
int delay = (int) Math.pow(2, retryCount) * 100;
System.out.println("重试中,等待" + delay + "ms");
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
retryCount++;
}
}
public static void main(String[] args) {
ExponentialBackoff example = new ExponentialBackoff();
new Thread(example::task).start();
new Thread(example::task).start();
}
}
适用场景:高频率重试场景,如网络请求、数据库连接重试。
5. 活锁的预防最佳实践
- 避免过度协调:不要设计过于"礼貌"的线程协调逻辑
- 引入随机性:在重试前加入随机等待时间
- 设置最大重试次数:避免无限重试
- 改变协调逻辑:避免线程之间过度依赖对方的状态
- 监控系统状态:通过监控工具及时发现活锁问题
三、死锁与活锁对比总结
| 特性 | 死锁 | 活锁 |
|---|---|---|
| 线程状态 | 完全阻塞 | 持续运行 |
| 系统进展 | 无进展 | 无有效进展 |
| CPU使用率 | 低 | 高 |
| 检测难度 | 相对容易(jstack标注DEADLOCK) | 较难(需要分析日志和线程行为) |
| 解决思路 | 破坏死锁条件 | 打破过度协调循环 |
| 典型场景 | 锁顺序不一致 | 过度礼貌的协调逻辑 |
四、多线程问题排查与预防最佳实践
1. 代码编写阶段
- 统一锁顺序:所有线程按相同顺序获取锁
- 使用tryLock:设置合理的超时时间
- 减少锁持有时间:在锁内避免耗时操作
- 使用细粒度锁:避免大锁竞争
- 避免嵌套锁:尽量将多个锁操作拆分为独立步骤
2. 运行时监控
- 使用VisualVM/JConsole:监控线程状态和锁竞争
- 设置JVM参数:-XX:+UnlockDiagnosticVMOptions -XX:+PrintGCApplicationStoppedTime 等
- 定期生成线程快照:jstack定期输出线程状态
- 监控CPU使用率:发现活锁的早期迹象
3. 生产环境最佳实践
- 设置合理的超时:对关键操作设置合理的超时时间
- 实现重试机制:对可重试操作实现指数退避重试
- 日志记录:记录锁获取和释放的关键日志
- 监控系统:实现对线程状态的实时监控
- 定期检查:定期检查代码中可能的锁竞争点
4. 高级排查技巧
-
Arthas:使用Arthas的thread命令查看线程状态
arthas> thread -n 3 -
火焰图分析:使用Java Mission Control生成锁竞争火焰图
-
线程转储分析:分析jstack输出,寻找"waiting for monitor entry"等关键信息
五、总结
Java线程死锁和活锁是并发编程中的常见问题,但通过正确的设计和预防措施,可以有效避免这些问题。
死锁:通过统一锁顺序、使用tryLock、减少锁粒度等方法预防。
活锁:通过引入随机等待时间、指数退避算法等方法预防。
关键原则:
- 设计阶段预防:在编写代码前考虑并发问题
- 运行时监控:及时发现潜在问题
- 代码实践:遵循最佳实践编写线程安全代码
记住:"预防胜于治疗"。在并发编程中,提前考虑可能的并发问题比在出现问题后修复要高效得多。通过合理的设计和充分的测试,可以大幅降低死锁和活锁的发生概率,提高系统的稳定性和可靠性。
- 感谢你赐予我前进的力量

