一、前言
在多线程并发编程中,除了数据安全问题,线程协作异常是另一类高频问题,其中死锁、活锁、饥饿是最典型的三类问题。这些问题会导致线程无法正常执行、系统性能下降甚至服务不可用,且排查难度高 —— 死锁可能隐藏数月,在高并发场景下才会触发。
本文将深入剖析这三类问题的产生原因、典型场景、排查方法,并提供可落地的解决方案与避坑指南。
二、死锁(deadlock)
1. 死锁的定义
死锁是指两个或多个线程互相持有对方所需的锁,且都不释放自己持有的锁,导致所有线程永久阻塞,无法继续执行的状态。
2. 死锁的四大必要条件(缺一不可)
只有同时满足以下 4 个条件,才会产生死锁:
互斥条件:锁资源只能被一个线程持有,其他线程无法获取;
持有并等待条件:线程持有已获取的锁,同时等待其他线程持有的锁;
不可剥夺条件:线程持有的锁不能被强制剥夺,只能由线程主动释放;
循环等待条件:线程 a 等待线程 b 的锁,线程 b 等待线程 a 的锁,形成循环等待链。
3. 死锁典型案例
/**
* 死锁演示:线程1持有锁a,等待锁b;线程2持有锁b,等待锁a
*/
public class deadlockdemo {
// 定义两个锁对象
private static final object lock_a = new object();
private static final object lock_b = new object();
public static void main(string[] args) {
// 线程1:获取lock_a → 等待lock_b
new thread(() -> {
synchronized (lock_a) {
system.out.println(thread.currentthread().getname() + " 获取到lock_a,等待lock_b");
try {
thread.sleep(1000); // 放大死锁概率
} catch (interruptedexception e) {
e.printstacktrace();
}
synchronized (lock_b) {
system.out.println(thread.currentthread().getname() + " 获取到lock_b,执行完成");
}
}
}, "线程1").start();
// 线程2:获取lock_b → 等待lock_a
new thread(() -> {
synchronized (lock_b) {
system.out.println(thread.currentthread().getname() + " 获取到lock_b,等待lock_a");
try {
thread.sleep(1000);
} catch (interruptedexception e) {
e.printstacktrace();
}
synchronized (lock_a) {
system.out.println(thread.currentthread().getname() + " 获取到lock_a,执行完成");
}
}
}, "线程2").start();
}
}运行结果 :两个线程互相等待对方的锁,永久阻塞,无后续输出。
4. 死锁的排查方法
方法 1:jstack 命令(最常用)
1.执行 jps 命令,获取进程 id:
jps # 输出示例:1234 deadlockdemo
2.执行 jstack <进程id> ,查看线程状态:
jstack 1234
3.关键输出(死锁提示):
found one java-level deadlock: ============================= "线程2": waiting to lock monitor 0x00007f9e3c006800 (object 0x000000076ab60eb0, a java.lang.object), which is held by "线程1" "线程1": waiting to lock monitor 0x00007f9e3c009000 (object 0x000000076ab60ec0, a java.lang.object), which is held by "线程2"
方法 2:jconsole 可视化工具
启动 jconsole(jdk/bin 目录下),连接目标进程;
切换到「线程」标签页,点击「检测死锁」,自动识别死锁线程及锁信息。
5. 死锁的解决方案
核心思路: 破坏死锁的四大必要条件之一 ,常用方案:
方案 1:统一锁获取顺序(破坏循环等待条件)
所有线程按相同的顺序获取锁,避免循环等待:
// 优化后:线程1和线程2都先获取lock_a,再获取lock_b
new thread(() -> {
synchronized (lock_a) {
system.out.println(thread.currentthread().getname() + " 获取到lock_a,等待lock_b");
try {
thread.sleep(1000);
} catch (interruptedexception e) {
e.printstacktrace();
}
synchronized (lock_b) {
system.out.println(thread.currentthread().getname() + " 获取到lock_b,执行完成");
}
}
}, "线程1").start();
new thread(() -> {
synchronized (lock_a) { // 统一先获取lock_a
system.out.println(thread.currentthread().getname() + " 获取到lock_a,等待lock_b");
try {
thread.sleep(1000);
} catch (interruptedexception e) {
e.printstacktrace();
}
synchronized (lock_b) {
system.out.println(thread.currentthread().getname() + " 获取到lock_b,执行完成");
}
}
}, "线程2").start();方案 2:使用可中断锁(破坏不可剥夺条件)
使用 reentrantlock 的 lockinterruptibly() 方法,允许线程被中断,主动释放锁:
import java.util.concurrent.locks.reentrantlock;
public class deadlockresolvebyinterrupt {
private static final reentrantlock lock_a = new reentrantlock();
private static final reentrantlock lock_b = new reentrantlock();
public static void main(string[] args) throws interruptedexception {
thread thread1 = new thread(() -> {
try {
// 可中断的锁获取
lock_a.lockinterruptibly();
system.out.println(thread.currentthread().getname() + " 获取到lock_a,等待lock_b");
thread.sleep(1000);
lock_b.lockinterruptibly();
system.out.println(thread.currentthread().getname() + " 获取到lock_b,执行完成");
} catch (interruptedexception e) {
system.out.println(thread.currentthread().getname() + " 被中断,释放锁");
if (lock_a.isheldbycurrentthread()) {
lock_a.unlock();
}
if (lock_b.isheldbycurrentthread()) {
lock_b.unlock();
}
}
}, "线程1");
thread thread2 = new thread(() -> {
try {
lock_b.lockinterruptibly();
system.out.println(thread.currentthread().getname() + " 获取到lock_b,等待lock_a");
thread.sleep(1000);
lock_a.lockinterruptibly();
system.out.println(thread.currentthread().getname() + " 获取到lock_a,执行完成");
} catch (interruptedexception e) {
system.out.println(thread.currentthread().getname() + " 被中断,释放锁");
if (lock_a.isheldbycurrentthread()) {
lock_a.unlock();
}
if (lock_b.isheldbycurrentthread()) {
lock_b.unlock();
}
}
}, "线程2");
thread1.start();
thread2.start();
// 等待3秒,若检测到死锁,中断线程1
thread.sleep(3000);
if (thread1.isalive() && thread2.isalive()) {
thread1.interrupt();
system.out.println("检测到死锁,中断线程1");
}
}
}方案 3:使用超时获取锁(破坏持有并等待条件)
使用 reentrantlock 的 trylock(long time, timeunit unit) 方法,超时未获取锁则放弃,避免永久等待:
import java.util.concurrent.timeunit;
import java.util.concurrent.locks.reentrantlock;
public class deadlockresolvebytimeout {
private static final reentrantlock lock_a = new reentrantlock();
private static final reentrantlock lock_b = new reentrantlock();
public static void main(string[] args) {
new thread(() -> {
try {
if (lock_a.trylock(2, timeunit.seconds)) { // 超时2秒
system.out.println(thread.currentthread().getname() + " 获取到lock_a,等待lock_b");
thread.sleep(1000);
if (lock_b.trylock(2, timeunit.seconds)) {
system.out.println(thread.currentthread().getname() + " 获取到lock_b,执行完成");
lock_b.unlock();
} else {
system.out.println(thread.currentthread().getname() + " 超时未获取lock_b,释放lock_a");
lock_a.unlock();
}
lock_a.unlock();
}
} catch (interruptedexception e) {
e.printstacktrace();
}
}, "线程1").start();
new thread(() -> {
try {
if (lock_b.trylock(2, timeunit.seconds)) {
system.out.println(thread.currentthread().getname() + " 获取到lock_b,等待lock_a");
thread.sleep(1000);
if (lock_a.trylock(2, timeunit.seconds)) {
system.out.println(thread.currentthread().getname() + " 获取到lock_a,执行完成");
lock_a.unlock();
} else {
system.out.println(thread.currentthread().getname() + " 超时未获取lock_a,释放lock_b");
lock_b.unlock();
}
lock_b.unlock();
}
} catch (interruptedexception e) {
e.printstacktrace();
}
}, "线程2").start();
}
}三、活锁(livelock)
1. 活锁的定义
活锁是指线程没有阻塞,但因互相谦让(或重试),导致始终无法获取所需资源,程序无法推进的状态。与死锁的核心区别:死锁是线程完全阻塞,活锁是线程一直在执行,但无实际进展。
2. 活锁典型案例
/**
* 活锁演示:两个线程互相释放锁,重试获取对方的锁,始终无法执行完成
*/
public class livelockdemo {
private static final reentrantlock lock_a = new reentrantlock();
private static final reentrantlock lock_b = new reentrantlock();
public static void main(string[] args) {
// 线程1:获取lock_a失败 → 释放lock_b(若持有)→ 重试
new thread(() -> {
while (true) {
try {
if (lock_a.trylock(100, timeunit.milliseconds)) {
system.out.println(thread.currentthread().getname() + " 获取到lock_a,尝试获取lock_b");
if (lock_b.trylock(100, timeunit.milliseconds)) {
system.out.println(thread.currentthread().getname() + " 获取到lock_b,执行完成");
lock_b.unlock();
lock_a.unlock();
break; // 执行完成,退出循环
} else {
system.out.println(thread.currentthread().getname() + " 获取lock_b失败,释放lock_a");
lock_a.unlock();
thread.sleep(100); // 谦让,重试
}
}
} catch (interruptedexception e) {
e.printstacktrace();
}
}
}, "线程1").start();
// 线程2:获取lock_b失败 → 释放lock_a(若持有)→ 重试
new thread(() -> {
while (true) {
try {
if (lock_b.trylock(100, timeunit.milliseconds)) {
system.out.println(thread.currentthread().getname() + " 获取到lock_b,尝试获取lock_a");
if (lock_a.trylock(100, timeunit.milliseconds)) {
system.out.println(thread.currentthread().getname() + " 获取到lock_a,执行完成");
lock_a.unlock();
lock_b.unlock();
break;
} else {
system.out.println(thread.currentthread().getname() + " 获取lock_a失败,释放lock_b");
lock_b.unlock();
thread.sleep(100); // 谦让,重试
}
}
} catch (interruptedexception e) {
e.printstacktrace();
}
}
}, "线程2").start();
}
}运行结果 :两个线程反复获取锁、释放锁、重试,始终无法同时获取两个锁,陷入无限循环。
3. 活锁的解决方案
核心思路: 打破 “同步重试” 的循环 ,常用方案:
1.随机重试延迟:每个线程重试时使用随机的休眠时间,避免同步谦让;
// 替换固定休眠时间为随机时间 thread.sleep(new random().nextint(500));
2.优先级机制:为线程设置不同的优先级,让部分线程优先获取资源;
3.限制重试次数:设置最大重试次数,超过次数则放弃并报警,避免无限循环。
四、饥饿(starvation)
1. 饥饿的定义
饥饿是指某些线程因优先级低、或始终竞争不到锁资源,导致长期无法执行的状态。例如:高优先级线程持续占用 cpu,低优先级线程始终无法执行;非公平锁下,某些线程始终抢不到锁。
2. 饥饿典型案例
/**
* 饥饿演示:高优先级线程持续占用锁,低优先级线程长期无法获取锁
*/
public class starvationdemo {
private static final object lock = new object();
public static void main(string[] args) {
// 低优先级线程
thread lowprioritythread = new thread(() -> {
int count = 0;
while (true) {
synchronized (lock) {
system.out.println(thread.currentthread().getname() + " 执行第" + (++count) + "次");
try {
thread.sleep(100); // 持有锁时间短,但仍被高优先级线程抢占
} catch (interruptedexception e) {
e.printstacktrace();
}
}
}
}, "低优先级线程");
lowprioritythread.setpriority(thread.min_priority); // 优先级1
// 高优先级线程
thread highprioritythread = new thread(() -> {
int count = 0;
while (true) {
synchronized (lock) {
system.out.println(thread.currentthread().getname() + " 执行第" + (++count) + "次");
try {
thread.sleep(100);
} catch (interruptedexception e) {
e.printstacktrace();
}
}
}
}, "高优先级线程");
highprioritythread.setpriority(thread.max_priority); // 优先级10
lowprioritythread.start();
highprioritythread.start();
}
}运行结果 :高优先级线程的执行次数远多于低优先级线程,低优先级线程长期 “饥饿”。
3. 饥饿的解决方案
核心思路: 保证资源分配的公平性 ,常用方案:
1.使用公平锁: reentrantlock 的公平锁模式保证线程按 fifo 顺序获取锁,避免插队;
private static final reentrantlock lock = new reentrantlock(true); // 公平锁
2.避免线程优先级差异:尽量将线程优先级设置为相同(默认 5),减少调度器的偏好;
3.减少锁持有时间:缩短同步代码块的执行时间,让锁尽快释放,增加低优先级线程的获取机会;
4.使用线程池:线程池的工作线程优先级一致,且有任务队列缓冲,避免个别线程长期抢占资源。
五、并发问题对比
问题类型 | 核心特征 | 线程状态 | 排查难度 | 核心解决方案 |
|---|---|---|---|---|
死锁 | 互相持有锁,永久阻塞 | blocked | 中(jstack 可直接检测) | 统一锁顺序、超时获取、可中断锁 |
活锁 | 无阻塞,但互相谦让,无进展 | runnable | 高(无明显报错,需分析日志) | 随机重试延迟、优先级机制、限制重试次数 |
饥饿 | 长期竞争不到资源,偶尔执行 | runnable | 中(需统计执行频率) | 公平锁、统一优先级、减少锁持有时间 |
六、实战避坑指南
1. 预防死锁的最佳实践
最小化锁范围:仅在必要的代码块加锁,缩短锁持有时间;
避免嵌套锁:尽量不使用多层锁嵌套,若必须使用,严格统一锁获取顺序;
使用定时锁:优先使用 trylock(timeout) 替代无超时的锁获取;
监控锁状态:通过 jmx/apm 工具监控锁的持有时间、竞争次数,提前发现死锁风险。
2. 通用优化建议
优先使用并发工具: concurrenthashmap 、 countdownlatch 等工具已封装安全的并发逻辑,避免手动加锁;
避免手动线程管理:使用线程池( threadpoolexecutor )替代手动创建线程,统一管理线程生命周期;
增加容错机制:关键业务线程设置超时、重试、降级逻辑,避免因并发问题导致服务不可用;
压测验证:上线前通过高并发压测,模拟极端场景,提前暴露死锁 / 活锁 / 饥饿问题。
七、总结
本文深入剖析了 java 并发编程中死锁、活锁、饥饿三类典型问题的产生原因、典型场景与解决方案。死锁是最致命的问题,需通过破坏四大必要条件来预防;活锁需打破同步重试的循环;饥饿需保证资源分配的公平性。在实际开发中,应遵循 “预防大于排查” 的原则:通过统一锁顺序、使用公平锁、缩短锁持有时间等手段,从根源减少并发问题的发生;同时掌握 jstack、jconsole 等排查工具,快速定位已出现的问题。
到此这篇关于java并发常见问题之死锁/活锁/饥饿的排查与解决方法的文章就介绍到这了,更多相关java并发死锁/活锁/饥饿内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论