什么是死锁
死锁就是两个(或更多)线程相互持有对方所需要的资源,又不主动释放资源,导致程序陷入无尽的阻塞,这就是死锁。
多个线程造成死锁的情况:等待的资源,形成一个环
死锁的影响
- 死锁的影响在不同的系统中是不一样的,这取决于系统对死锁的处理能力
- 数据库中:检测并放弃事务
- JVM中:无法自行处理,但是JVM能检测出死锁
一个必然造成死锁的栗子
- 当类的对象flag=1时(T1),先锁定O1,睡眠500毫秒,然后锁定O2;
- 而T1在睡眠的时候另一个flag=0的对象(T2)线程启动,先锁定O2,睡眠500毫秒,等待T1释放O1;
- T1睡眠结束后需要锁定O2才能继续执行,而此时O2已被T2锁定;
- T2睡眠结束后需要锁定O1才能继续执行,而此时O1已被T1锁定;
- T1、T2相互等待,都需要对方锁定的资源才能继续执行,从而死锁。
同时注意一点,死锁的程序退出状态码是130.
Process finished with exit code 130
1 | /** |
死锁的4个必要条件
- 互坼条件:在同一时刻,只能有一个线程拥有资源
- 请求与保持条件:自己不释放资源,又去申请别的资源
- 不剥夺条件:没有外界的干扰
- 循环等待条件:多个线程没有形成环路
死锁的形成,这4个条件缺一不可
如何定位死锁
- jstack(jvm命令,可以先用jps lvm查看JVM进程)
- ThreadMXBean(代码)
修复死锁的策略
线上发生死锁应该怎么办
- 保存案发现场,然后立刻重启服务器
- 暂时保证线上服务的安全,然后再利用刚才保存的信息,排查死锁,修改代码,重新发布
1.避免策略
- 避免相反的获取锁的顺序
* 通过hashcode来决定获取锁的顺序,冲突时需要“加时赛” * 实际工程中可以利用数据库的主键
- 哲学家就餐问题
2.检测与恢复策略
一段时间检测是否有死锁,如果有就剥夺某一个资源,来打开死锁
3.鸵鸟策略
如果发生死锁的概率非常低,我们就直接忽略它,直到死锁发生的时候,再人工修复。
栗子1:哲学家就餐问题(换序避免死锁)
1 | /** |
另:哲学界就餐问题解决的4种方案:
- 服务员检查(避免策略)
- 改变一个哲学界拿叉子的顺序,换序(避免策略)
- 餐票,只有拿到餐票的才能就餐,就餐完返回餐票(避免策略)
- 领导调节(检测与恢复策略)
栗子2:银行转账(hashCode换序避免死锁)
1 | /** |
实际工程中如何避免死锁
- 设置超时时间
- 多使用并发类而不是自己设计锁
- 尽量降低锁的使用粒度:用不同的锁而不是一个锁
- 如果能使用同步代码块,就不使用同步方法:自己指定锁对象
- 给线程起个有意义的名字:debug和排查时事半功倍
- 避免锁的嵌套
- 分配资源前先看能不能收回来:银行家算法
- 尽量不要几个功能用同一把锁:专锁专用
什么是活锁
虽然线程并没有阻塞,也始终在运行,但是程序却得不到进展,因为线程始终重复做同样的事
栗子2:牛郎织女没饭吃(加入随机因素解决活锁)
1 | /** |
什么是饥饿
当线程需要某些资源(例如CPU),但是却始终得不到
- 线程的优先级设置得过于低,或者有某线程持有锁同时又无限循环从而不释放锁,或者某程序始终占用某文件的写锁
- 饥饿可能会导致响应性差:比如,我们的浏览器有一个线程负责处理前台响应(打开收藏夹等动作),另外的后台线程负责下载图片和文件、计算渲染等。在这种情况下,如果后台线程把CPU资源都占用了,那么前台线程将无法得到很好地执行,这会导致用户的体验很差