文章目录
什么是死锁?
- 发生在
并发
中 - 互不相让:当两个(或更多)线程(或进程)
相互持有对方所需要的资源
,又不主动释放,导致所有人都无法继续前进,导致程序陷入无尽的阻塞,这就是死锁。 - 死锁图解
- 多个线程造成死锁的情况:如果多个线程之间的依赖关系是
环形
,存在环路的锁的依赖关系,那么也可能会发生死锁。 - 几率不高但危害大
- 不一定发生,但遵守“
墨菲定律
” - 一旦发生,多是
高并发
场景,影响用户多 - 整个
系统奔溃
,子系统崩溃、性能降低 - 压力测试
无法找出
所有潜在的死锁
- 不一定发生,但遵守“
死锁的4个必要条件
- 互斥条件:一个资源每一次
只能被一个
进程或者线程使用。 - 请求与保持条件:第一个线程请求第二把锁同时保持第一把锁,这个时候,线程请求的时候已经自身阻塞了,就是说对于
已经获取的资源保持不变
,不会释放。 - 不剥夺条件:
不被外界干扰
剥夺锁。 - 循环等待条件:两个或者多个互相等待或者环形
等待锁的释放
。
如何定位死锁?
- jstack命令行
- 利用工具类:ThreadMXBean类
- 案例参考:银行转账问题
修复死锁的策略
- 避免策略:
哲学家就餐
的换手方案、转账换序方案。 - 检测与恢复策略:一段时间
检测
是否有死锁,如果有就剥夺
某一个资源,来打开死锁。- 允许发生死锁
- 每次调用锁都记录
- 定期检查“锁的调用链路图”中是否存在环路
- 一旦发生死锁,就用死锁恢复机制进行恢复
- 鸵鸟策略:鸵鸟这种动物在遇到危险的时候,通常就会把头埋在地上,这样一来它就看不到危险了。而鸵鸟策略的意思就是说,如果我们发生死锁的概率极其低,那么我们就直接
忽略
它,直到死锁发生
的时候,再人工修复
。这是一种消极的处理方式,不推荐。
实际工程中如何避免死锁
- 设置超时时间
- Lock的
tryLock
(long timeout, TimeUnit unit) - synchronized
不具备
尝试锁的能力 - 造成超时的可能性多:发生了死锁、线程陷入死循环、线程执行很慢
- 获取锁失败:打日志、发报警邮件、重启等
- Lock的
- 多使用
并发类
而不是自己设计锁- ConcurrentHashMap、ConcurrentLinkedQueue、AtomicBoolean等
- 实际应用中java.util.concurrent.atomic十分有用,简单方便且效率比使用Lock更高
- 多用并发集合少用同步集合,并发集合比同步集合的可扩展性更好
- 并发场景需要用到map,首先想到用ConcurrentHashMap
- 尽量降低锁的使用
粒度
:用不同的锁而不是一个锁 - 如果能使用
同步代码块
,就不使用同步方法:自己指定锁对象(方便掌控锁) - 给你的线程起个有意义的
名字
:debug和排查时事半功倍,框架和JDK都遵循这个最佳实践 - 避免锁的
嵌套
:MustDeadLock类(互相转账问题) - 分配资源前先看能不能
收回来
:银行家算法 - 尽量不要几个功能用同一把锁:
专锁专用
其他活性故障
- 死锁是最常见的活跃性问题,不过除了刚才的死锁外,还有些类似的问题,会导致程序无法顺利执行,统称为
活跃性问题
- 活锁(LiveLock)
- 饥饿
- 当线程需要某些资源(例如CPU),但是却始终得不到
- 线程的
优先级
设置的过于低,或者有某线程持有锁同时又无限循环不释放锁
,或者某程序始终占用
某文件的写锁 - 饥饿可能会导致
响应性差
:比如,我们的浏览器有一个线程负责处理前台响应(打开收藏夹等动作),另外的后台线程负责下载图片和文件、计算渲染等。在这种情况下,如果后台线程把CPU资源都占用了,那么前台线程将无法得到很好地执行,这会导致用户的体验很差
常见面试题
写一个必然死锁的例子,生产中什么场景下会发生死锁?
死锁例子: 银行转账问题。
什么场景容易发生死锁:最明显的就是一个方法中获取多个锁,或者在一个方法调用链中获取多个锁。
发生死锁必须满足哪些条件?
本篇提到。
如何定位死锁?
本篇提到。
有哪些解决死锁问题的策略?
本篇提到,同时还有两篇死锁解决案例,银行转账问题和哲学家就餐问题。
讲一讲哲学家就餐问题?
实际工程中如何避免死锁?
本篇提到。
什么是活跃性问题?活锁、饥饿和死锁有什么区别?
本篇提到。
笔记来源:慕课网悟空老师视频《Java并发核心知识体系精讲》