并发安全问题(死锁)
一、死锁
1. 死锁定义
多个线程互相持有对方需要的锁,又都不释放自己的锁,
互相无限等待,程序彻底卡死、无法继续执行。
2. 死锁四个必要条件(缺一不可)
互斥条件
资源是独占的,同一时刻只能被一个线程持有。
请求与保持
线程已经持有一把锁,不释放旧锁,又去申请新锁。
不可剥夺
锁只能自己主动释放,不能被其他线程强行抢走。
循环等待
线程之间形成环路锁依赖:T1 等 T2 的锁,T2 等 T1 的锁。
破坏任意一个条件,就能杜绝死锁。
3. 死锁避免 (开发实操方案)
统一锁的获取顺序(最常用、最简单)
所有线程固定顺序申请锁,不会形成循环等待。
主动释放不用的锁
不持有无用锁,破坏「请求与保持」。
使用带超时的锁
tryLock(time),拿不到锁直接放弃、回退业务,不死等。
放弃嵌套锁
尽量减少多层锁嵌套,从根源减少环路依赖。
使用 juc 工具替代手动加锁
用 CountDownLatch、Semaphore、并发容器,减少手写 synchronized 嵌套。
4. 死锁检测
jstack 命令
导出线程快照,搜索
Found one Java-level deadlock,直接定位死锁线程、锁对象、代码行。JConsole / JVisualVM
图形化工具,一键检测死锁线程。
线上监控
线程池队列积压、接口超时、CPU 低负载、线程 BLOCKED 大量堆积,大概率死锁。
二、活锁(Live Lock)
1. 定义
线程没有阻塞、没有卡死,一直在运行、不断重试,
但是永远无法完成任务,一直在空转、互相谦让。
2. 产生场景
两个线程发现对方占用资源,主动退让、重试,
你让我、我让你,无限循环,谁都执行不完。
3. 解决
- 加入随机等待时间,避免步调完全一致;
- 固定优先级,避免无意义重试退让。
三、饥饿(Starvation)
1. 定义
某个线程一直拿不到需要的资源,长期得不到执行,一直饿死。
线程不死、不阻塞,就是永远没机会跑。
2. 产生原因
- 锁非公平:高优先级 / 高频线程一直抢占锁;
- 同步锁一直被大线程霸占;
- 常量抢占资源,弱势线程永久排队。
3. 解决
- 使用公平锁
new ReentrantLock(true); - 限制锁持有时长,拆分大同步块;
- 资源合理分配,避免线程长期霸占。
四、优先级反转(Priority Inversion)
1. 定义
高优先级线程,反而被低优先级线程阻塞,导致响应延迟、故障。
2. 产生流程
低优先级线程 持有 共享锁;
高优先级线程 也要这把锁,被迫阻塞等待;
中间优先级线程 不断抢占 CPU,一直调度运行;
结果:
低优先级线程得不到 CPU、迟迟不释放锁;
高优先级线程一直被卡,优先级完全失效。
3. 解决方案
锁优先级继承
低优先级线程拿到锁后,临时提升为高优先级,
避免被中间线程抢占 CPU,快速执行完释放锁。
优先级天花板策略
限制持有锁的线程最高优先级,统一调度。
Java / 操作系统底层都会做优先级继承优化,缓解该问题。
极简原理总结
死锁:互斥 + 持有并请求 + 不可剥夺 + 循环等待;破坏任意一个即可预防。
死锁避免:顺序加锁、tryLock 超时、减少嵌套锁;
死锁检测:jstack、可视化工具。
活锁:线程一直运行、互相退让,完不成任务;加随机延时解决。
饥饿:线程长期抢不到资源,一直等待;用公平锁解决。
优先级反转:高优先级等低优先级锁,被中间级线程插队拖慢;靠优先级继承解决。