【15】死锁的防范
【15】死锁的防范
秋雨霏霏 发表于4个月前
【15】死锁的防范
  • 发表于 4个月前
  • 阅读 19
  • 收藏 0
  • 点赞 0
  • 评论 0

【腾讯云】如何购买服务器最划算?>>>   

摘要: Jakob Jenkov 并发指南翻译:Deadlock Prevention

加锁顺序

多线程发生死锁,主要是和他们加锁的顺序有关系。

如果确定所有的锁总是按照相同的顺序加锁,那么就不会遇到死锁。 来看看这个例子:

Thread 1:

  lock A 
  lock B


Thread 2:

   wait for A
   lock C (when A locked)


Thread 3:

   wait for A
   wait for B
   wait for C

如果像线程3这样,需要好几个锁,那就需要注意这几个锁的顺序。

例如,如果线程1先拿到锁A时,那线程2和线程3都不会拿到锁C。 这个时候,线程2和线程3其实都只能等待线程1释放锁A才能继续去申请锁B和锁C。

加锁顺序是预防死锁的一种简单而有效的机制。 然而,这是在你知道需要用到的全部锁的情况时,才能起作用的。 但实际中可能不是这样的。所以这个方法也并不是万能的。

加锁超时

另一个预防死锁的方法,就是在申请锁的时候,设置一个超时时间。 这样,就可以防止线程在某个得不到的锁上永久等待。 在超时时间之后,如果线程还得不到锁,那线程就可以返回加锁失败。 这样就可以根据这个返回信息,有机会来处理一些逻辑。 针对死锁,当然就可以在返回加锁失败时,就可以释放掉之前已经获得的锁。 这样就避免了其他线程长时间等待这些锁的情况发生了。 如此,就可以让程序避免死锁。

下面这个例子,展示了两个线程对两个锁以不同的顺序进行加锁,但是这次他们带有超时设定:

Thread 1 获得 锁A
Thread 2 获得 锁B

Thread 1 尝试获取 锁B 失败,进入阻塞等待
Thread 2 尝试获取 锁A 失败,进入阻塞等待

Thread 1 在 锁B 等待超时
Thread 1 返回,并释放已获得的 锁A
Thread 1 随机休息一定时间(例如:257 毫秒)后,进行重试.

Thread 2 在 锁A 等待超时
Thread 2 返回,并释放已获得的 锁B
Thread 2 随机休息一定时间(例如:43 毫秒)后,进行重试.

在上面这个例子中,线程2将会先于线程1大约200毫秒进行重试。 这样线程2就会有机会顺利的拿到两个锁。 而线程1将会重新尝试获取锁A。 当线程2完成后,线程1也会重试,并有机会顺利拿到两个锁。

但要记住一点,锁等待超时时间的设定,不代表就一定发生死锁了。 这个超时时间是避免长时间在某个锁上阻塞而设定的,比如如果持有锁的线程需要执行一个非常耗时的任务,这种情况下设置超时时间也能有效的避免长时间等待的情况,也可以避免线程饥饿的情况发生。

另外,如果过多线程对同一个资源进行争用,设置超时时间,就可能导致线程会不断的一次又一次的重试。 比如两个线程下超时时间在0~500毫秒的范围就可以正常运行,但是这不代表10或者20个线程的情况下也能正常。 线程太多,资源争用频繁的情况下,就可能导致两个或者多个线程容易在同一时间点进行重试。

超时机制还会有一个问题,那就是,Java的synchronized锁,是无法设置超时时间的。 这种情况下,就需要使用并发包(JUC)下的Lock类,进行显式的锁控制。

侦测死锁

侦测死锁是一个重要的死锁预防机制。 特别是在无法确定加锁顺序以及无法设置超时时间的情况下。

每次线程获得一个锁时候,就把这个线程和锁的对应信息记录到某个容器中(如:map)。 另外,在每次线程请求锁的时候,也记录到这个容器中。

当线程请求锁失败时,线程可以遍历这个容器,来检测是否发生死锁了。 例如,如果线程A请求锁7,但是锁7已经被线程B拿到,那么线程A就可以检查线程B是不是也在请求线程A已经持有的锁。如果线程B确实也在请求线程A的锁,那就可以侦测到死锁了。

当然,实际中的情况远比两个线程的情况要复杂。 可能在多个线程之间循环依赖,导致一个复杂的死锁。

下面这个图,就展示了一个侦测4个线程死锁的情况:

image

那么,侦测到死锁后,线程又需要做些什么呢?

一种策略是,释放掉所有已经获得的锁,并随机休息一段时间后再次进行重试。 这个策略有点类似超时时间的处理方式。所以,同样的,在争用激烈的情况下,频繁的重试成功率也不会太高。

另一个更好的策略是,在线程设进行重试时,设置一个优先级。这样,让优先级低的某几个(或者一个)放弃执行,进入等待。而其他线程,就像没有发生死锁那样继续执行。这样,就可以避免重试再次失败的情况。

不过要注意,如果线程的优先级是固定的,那可能导致某些高优先级的线程一直得到处理,低优先级的线程一直得不到执行机会。为了避免这种情况的发生,可以在侦测到死锁的时候,随机分配优先级。


补充几点

避免死锁

  • 一次至多获得一个锁
  • 尽可能使用开放调用
  • 使用显示Lock类,替代内部锁机制
    • tryLock
    • timeout

有关加锁顺序的注意事项,也同样适用于数据库操作。 如,多个线程可能对某些数据执行update操作,这个时候,如果update的数据行,也是按不同顺序update,那也同样容易导致数据库死锁。

所以,在调用批量update时,最好对数据集合以相同的规则进行一次排序。

标签: 并发 死锁
共有 人打赏支持
粉丝 116
博文 78
码字总数 142063
×
秋雨霏霏
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: