活性(Liveness):程序最终一定能向前推进、完成任务;与之对应的是安全性(Safety):坏事情永远不会发生。 死锁、活锁、饥饿,全部属于活性故障:线程卡在那里,一直无法正常执行完业务。
一、三个概念对比(核心区分)
1. 死锁(Deadlock)【最常见】
定义:一组线程互相持有对方需要的锁,同时等待对方释放锁,所有人都静止,不再做任何动作,永久卡住。
一句话:大家拿着锁、互相等,全都不动了
死锁 4 个必要条件(四个同时满足才会死锁,破坏任意一个即可避免):
- 互斥条件:资源是排他锁,同一时间只能一个线程持有(synchronized、ReentrantLock 排他模式)
- 持有并等待:线程已经拿到 A 锁,不释放,再去请求 B 锁
- 不可剥夺:锁只能持有者主动释放,别人抢不走、不能强行拿走
- 循环等待:线程形成环形等待链:T1 等 T2、T2 等 T3 … Tn 等 T1
✅ 典型场景: T1:锁 A → 请求锁 B T2:锁 B → 请求锁 A
2. 活锁(Livelock)
定义:线程没有阻塞、没有挂起,一直在运行、不断重试,但是永远无法完成目标。线程状态是 RUNNABLE,不是 BLOCKED/WAITING。
一句话:一直在忙活,原地踏步,没有进展
✅ 典型场景: 两个线程同时拿到资源检测冲突,然后主动释放、退让,再同时重试,反复礼让,谁都拿不到资源。 比如:简易自旋锁,冲突就 sleep 一小段,两个线程休眠时间完全一样,每次唤醒又撞车。
区别死锁:死锁线程不动;活锁线程一直在跑。
3. 饥饿(Starvation)
定义:线程一直拿不到它需要的资源,长期得不到执行机会,一直被插队,迟迟无法推进,甚至永远执行不到。
一句话:资源被优先级高 / 抢占能力强的线程持续霸占,弱者一直轮不到
✅ 典型场景:
- ReentrantLock 默认非公平锁:高并发下,新来的线程直接抢到锁,队列里等待很久的线程持续拿不到(饥饿)
- 线程优先级设置差距过大,低优先级线程几乎不被调度
- 一个线程持有锁时间极长,其他线程排队一直等
区分三兄弟速记:
- 死锁:都不动(阻塞等待)
- 活锁:一直在动,但白跑(自旋重试)
- 饥饿:想跑,但是轮不到它(资源被抢占)
二、如何预防这三类活性故障
✅ 死锁预防(破坏 4 条件之一)
- 破坏持有并等待:一次性申请所有需要的锁,要么全部拿到,一个都拿不到就不持有任何锁
- 破坏不可剥夺:支持锁超时、尝试获取(
tryLock(timeout)),拿不到就释放已经持有的锁,回头重试 - 破坏循环等待【工程最常用】:全局统一锁的获取顺序
比如:不管哪个线程,要锁 A、B,永远先拿 A,再拿 B,禁止反过来
- 互斥条件很多业务无法破坏(必须排他),一般不在这里下手
编码规范:优先统一加锁顺序 + 使用带超时的 tryLock,不要无脑用 synchronized 嵌套。
✅ 活锁预防
- 退让重试时,加入随机退避时间(jitter),不要固定休眠时长
- 限制重试次数,超过次数直接失败,不要无限自旋
- 排队机制(队列锁,公平锁),避免互相礼让撞车
✅ 饥饿预防
- 锁选择:需要避免饥饿就用ReentrantLock (true) 公平锁(FIFO 排队,先来先得)
代价:公平锁上下文切换多,吞吐更低;非公平性能高,但有饥饿风险
- 不要滥用线程优先级
- 持有锁的代码尽量短,锁内不做 IO、慢逻辑,尽快释放锁
- 限制单个线程占用资源时长
三、重点:死锁排查(Java 线上排查流程,实操)
前提:现象:服务卡住、接口超时、线程池不干活、CPU 不一定高(死锁线程大多 BLOCKED,CPU 低,这点和活锁区分)
排查步骤(线上标准流程)
Step1:确认进程 PID
jps -l # 或者 ps -ef | grep javaStep2:导出线程栈(核心命令)
两种方式任选,推荐先 jstack:
# 直接打印线程栈到控制台 jstack <pid> # 输出到文件,方便下载分析 jstack <pid> > thread.txt重点:jstack 会自动检测 JVM 死锁,在文件末尾搜索关键字:
Found one Java-level deadlockJVM 可以直接发现synchronized / ReentrantLock 形成的死锁(AQS 的死锁 jstack 能识别)
Step3:看懂死锁日志模板
Found one Java-level deadlock: ============================= "Thread-1": waiting to lock monitor 0x00007f8b94003c68 (object 0x000000076ab34290, a java.lang.Object), which is held by "Thread-0" "Thread-0": waiting to lock monitor 0x00007f8b94003b28 (object 0x000000076ab34280, a java.lang.Object), which is held by "Thread-1"日志直接告诉你:
- 哪些线程
- 各自在等哪个锁
- 锁被哪个线程持有 往下看栈帧,可以定位到代码行号,直接找到嵌套加锁的地方。
Step4:辅助工具(复杂场景)
- JConsole / JVisualVM:图形界面,线程页,检测死锁按钮一键查看
- Arthas(阿里,线上神器,生产首选,不用装 JDK 工具包)
# arthas 命令 thread -b # -b 专门检测阻塞死锁线程,直接打印死锁信息,比jstack更友好 thread -n 5 # 查看top5 CPU线程(区分死锁和CPU高的活锁场景)⚠️ 注意:
thread -b只能发现锁等待型死锁;活锁、饥饿,arthas 和 jstack 不会标记为 deadlock,需要看线程状态 RUNNABLE、循环重试代码。
Step5:区分容易混淆现象
- 死锁:线程状态 BLOCKED / WAITING,jstack 标记 Found deadlock,CPU 低
- 活锁:线程 RUNNABLE,循环自旋重试,CPU 可能偏高,JVM 不报死锁
- 饥饿:部分线程一直在队列等待,锁被其他线程持续拿到,没有环形等待链,JVM 不报死锁
Step6:修复方案(拿到代码位置后)
- 统一加锁顺序(优先方案)
- 改成 tryLock + 超时,超时释放锁,抛出告警,不永久阻塞
- 拆锁:把大锁拆细,减少嵌套锁,尽量不要嵌套获取多把锁
- 替换锁结构:用无锁 CAS、并发容器,避开锁嵌套
四、坑点提醒
synchronized是不可中断、没有超时的;一旦嵌套顺序错,死锁后只能重启进程- ReentrantLock 支持 tryLock (时间)、lockInterruptibly,可控性远高于 synchronized,适合多锁场景
- 数据库死锁 ≠ Java 线程死锁:数据库死锁是 InnoDB 事务锁,要看 mysql
show engine innodb status,jstack 看不到 - 分布式死锁(Redis 分布式锁):跨进程、跨服务,JVM 栈完全看不到,要单独排查锁 key、释放逻辑、锁超时、续约
五、终极浓缩总结(背诵版)
✅ 活性三故障一句话
- 死锁:循环等待,互相拿锁,全部阻塞不动;4 条件同时满足;jstack 可直接检出。
- 活锁:无阻塞,一直自旋重试、互相避让,永远无法成功;加随机退避、限制重试次数解决。
- 饥饿:资源持续被抢占,部分线程长期拿不到资源;公平锁、缩短锁持有时间缓解。
✅ 死锁排查口诀
jps 拿 pid → jstack/arthas thread -b → 搜 Found deadlock → 看线程栈定位代码行 优先统一锁顺序,其次 tryLock 带超时,尽量减少锁嵌套
✅ 避坑总原则
能不加嵌套锁就不加;多锁必须定全局顺序;优先带超时可中断锁;锁内逻辑要短;区分 JVM 线程死锁、DB 死锁、分布式锁死锁。