一、死锁的定义
进程死锁
进程死锁是指两个或两个以上的进程在执行过程中,由于竞争资源(锁、网络连接、通知事件、磁盘、带宽等)或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
线程死锁
线程死锁是两个或更多线程阻塞着等待其它处于死锁状态的线程所持有的锁。死锁通常发生在多个线程同时但以不同的顺序请求同一组锁的时候。
当一个线程永远地持有一个锁,并且其他线程都尝试获得这个锁时,那么它们将永远被阻塞。在线程 A 持有锁 L 并想获得锁 M 的同时,线程 B 持有锁 M 并尝试获得锁 L,那么这两个线程将永远地等待下去。这种情况就是最简单的死锁形式,也称为「抱死」。
如果线程 1 锁住了A,然后尝试对B进行加锁,同时线程2已经锁住了B,接着尝试对A进行加锁,这时死锁就发生了。
二、都有哪些死锁?
Java 只要有并发多线程存在的地方就有可能发生死锁
并发中的死锁并不是一开始就会出现,多半由于自己业务的瞬间高负载或者峰值流量,线程集中处理加速开始导致死锁的出现。并且 Java 中的死锁很难像数据库服务机制可以放弃某些事务的进行解决死锁问题,除了可以进行一些基本的预防之外,多半需要线程回退或者服务重启。
数据库事务死锁
针对 MySQL,存在页级锁、表级锁和行级锁,而表级锁针对整张表加锁,故而不会产生死锁。所以大部分的死锁来自于 InnoDB 存储引擎的行锁,比如两个 session 分别针对某条记录做修改和删除操作,此时就会发生死锁。又或者两个 session 针对某一行的加锁顺序相同导致死锁。不过 MySQL 中基本都有很好的事物隔离级别,还有一些范围锁(GAP)、页面锁(16K为一页)等,针对各种死锁的情况基本都有对应的 SQL 解决方式等。
线程池死锁
一个线程池是单线程的,如果一个线程池中的两个任务互相依赖则也会发生死锁。
网络连接死锁
线程对于自己的获得的网络连接不进行资源释放,其他线程在等待连接的过程中也会发生死锁。
三、避免死锁的方式
加锁顺序
当多个线程需要获得相同的锁,但是要求它们按照顺序获取即可。不过这种方式要求我们事先知道所有可能用到的锁,甚至对这些锁进行排序。
加锁时限
在尝试获取锁的时候加一个超时时间,若超过了这个时限该线程则放弃对该锁请求。
这种方法首先在线程量比较大的情况下也不会很有效,多个线程由于超时时间一致,回到多个线程同时重试竞争获取锁,带来新的问题。
其次,synchronized 同步代码不支持超时参数设置,需要自定义锁。
死锁检测
死锁检测的处理方式本质跟加锁时限类似,只不过死锁检测是真正的检测到了死锁的情况才释放所有锁进行重试,而加锁时限并不确定是真的发生了死锁。
死锁的检测原理是当一个线程获取了锁,会在数据结构中记录相关参数。当其他的线程请求该锁的时候也会将来访记录保存。
比如线程 1 持有 A 锁,线程 2 持有 B 锁。当同时线程 2 请求锁 A 线程 1 请求 锁 B 的时候,线程 1 会检查线程 2 本身当前是否持有 B 锁的记录,同理线程 2 也是这样,于是这时候发生了死锁。
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程
不过由于本质的处理方式和加锁时限一样 ,都是对线程进行回退,所以避免不了线程下一次进行大量的资源竞争,带来死锁。
设置线程的优先级
在发生死锁的时候随机设置线程的优先级,优先级低的线程回退,高的继续等待。
总结
- 避免一个线程同时获取多个锁。
- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
- 尝试使用定时锁,使用 lock.tryLock(timeout) 来替代使用内部锁机制。
- 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
四、相关阅读
更博不易,如果觉得文章对你有帮助并且有能力的老铁烦请捐赠盒烟钱,点我去赞助。或者扫描文章下面的微信/支付宝二维码打赏任意金额(点击「给你买杜蕾斯」),也可扫描小站放的支付宝领红包二维码,线下支付享受优惠的同时老四也可以获得对应赏金,老四这里抱拳谢谢诸位了。捐赠时请备注姓名或者昵称,因为您的署名会出现在赞赏列表页面,您的捐赠钱财也会被用于小站的服务器运维上面,再次抱拳感谢。