1、什么是死锁
- 发生在并发中
- 互不相让,当两个或多个线程(或进程)相互持有对方所需要的资源,又不主动释放,导致所有程序都无法继续前进,导致程序陷入无尽的阻塞,这就是死锁。
1.1、死锁的影响
死锁的影响在不同的系统中是不一样的,这取决于系统对死锁的处理能力
- 数据库中:检测并放弃事务(让其中一个线程放弃资源)
- JVM中:无法自动处理
- 因为担心自动放弃了非常重要的线程
- 死锁发生几率不高,但是危害大,遵守墨菲定律
- 一旦发生,多是高并发场景,影响用户多
- 可能的影响:整个系统崩溃、子系统崩溃、性能降低
- 压力测试无法找出所有潜在的死锁
1.2、示例
1 | /** |
从程序中可以看到,共有两个线程,两个锁对象,每个线程的run方法都会先获取一个锁对象,再获取另一个锁对象,由于Thread.sleep(1000)的存在,能够让两个线程各自得到一个锁对象,然后陆续停留在获取另一个锁对象的前面。thread1拿到r1锁,thread2拿到r2锁,都不释放,都在等待,因此永久停滞在了此处。
1.3、实际例子:转账
以A向B转账为例:
- 需要两把锁
- 锁住A的账户,防止多个线程扣钱
- 锁住B的账户,防止多个线程转钱,因为多线程同时写操作会很危险
- 获取两把锁成功,且余额大于0,则扣除转出人,增加收款人的余额,是
原子
操作 - 顺序相反导致死锁:
- A给B转账
- 同时B给A转账
- 导致两个线程分别锁住了A和B,并且都需要对方持有的锁,就会变成死锁
1.4、死锁的四个必要条件
- 互斥条件:一个资源一个时刻只能被一个线程使用
- 请求保持条件:在请求新的资源的同时保持已有的资源不释放
- 不可剥夺条件:除了线程自己释放,外界不能强制剥夺线程已有的资源
- 环路等待条件:多个线程等待的资源,呈现一条环路形态:A等B的释放,B等C的释放,C等A的释放
四个必要条件为我们破解死锁提供了依据,只要破坏其中任何一个条件,死锁都不再成立
2、常见修复策略
2.1、避免策略
思路:避免相反的获取锁的顺序
- 实际上,程序不在乎获取锁的顺序,只在乎最后把所需要的锁都拿到即可
- 可以改变冲突的线程中的某些线程获取资源的顺序,避开冲突。获取了其他资源后再获取此资源,也许就不会冲突了
- 通过hashcode来决定获取锁的顺序,冲突时需要“加时赛”
- 如新加一把锁,冲突的多个线程,谁先拿到这把锁,就获取所有资源先执行。
2.2、检测与恢复策略
允许发生死锁,一段时间检测是否有死锁,如果有,就剥夺某一个资源,来打开死锁
- 如通过死锁检测算法即可
恢复方法:
- 进程终止:
- 逐个终止线程,直到死锁消除
- 终止顺序:
- 优先级
- 已占用资源,还需要的资源
- 已经运行的时间
- 资源抢占:
- 把已经分发出去的锁给收回来
- 让线程回退几步,这样就不用结束整个线程,成本比较低
- 缺点:可能同一个线程一直被抢占,那就造成饥饿
2.3、鸵鸟策略
鸵鸟这种动物在遇到危险的时候,通常把头埋在地上,这样一来它就看不到危险了,而鸵鸟策略的意思就是说,如果我们发生死锁的概率极其低,那么我们就直接忽略它,直到死锁发生的时候,再人工修复
3、实际工程避免死锁
- 设置超时时间
- Lock的tryLock(long timeout, TimeUnit unit)
- 时间不能太长,不能过短;太长浪费时间,过短可能会误判一些需要时间长的操作是死锁
- 获取锁失败:打日志,发报警邮件,重启等
- 多使用并发类而不是自己设计锁
- 尽量降低锁的使用粒度:用不同的锁而不是一个锁
- 如果能使用同步代码块,就不使用同步方法:自己指定锁对象
- 给线程起一个有意义的名字:debug和排查时事半功倍,框架和JDK都遵守这个最佳实践
- 避免锁的嵌套
- 分配资源前先看能不能收回来:银行家算法
- 尽量不要几个功能用同一把锁:专锁专用
4、其他活性故障
- 死锁是最常见的活跃性问题,不过除了刚才的死锁之外,还有一些类似的问题,会导致程序无法顺利执行,统称为活跃性问题
- 活锁
- 饥饿
4.1、活锁
在完全相同的时刻进入餐厅,并同时拿起左边的餐叉那么这些哲学家就会等待五分钟,同时放下手中的餐叉,再等五分钟,又同时拿起这些餐叉。(在等待一段时间后,还是拿不到所需要的锁,就判断可能发生了死锁,就释放自己的锁),等待时间相同,就可能造成活锁。
- 虽然线程并没有阻塞,也始终在运行(所以叫活锁,线程是活的),但是程序却得不到进展,因为线程始终重复同样的事。
- 活锁的线程仍然会消耗CPU资源,而死锁的线程不需要消耗CPU资源
- 活锁和死锁的结果是一样的
活锁发生的原因:重试机制不变,消息队列始终重试,吃饭始终谦让
解决办法:
- 以太网的指数退避算法
- 加入随机因素
4.2 饥饿
- 当线程需要某些资源(例如CPU),但是却始终得不到想要的资源
- 线程的优先级设置的过低,或则有某线程持有锁同时又无限循环从而不释放锁,或则某程序始终占用某文件的写锁
参考
- 本文作者: xczll
- 本文链接: https://xczllgit.github.io/2020/03/19/concurrent/2020-03-19-threadDeadLock/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!