首页 > 编程知识 正文

防止死锁和避免死锁,避免死锁的方法一般是以降低资源利用率为代价的

时间:2023-05-05 05:21:07 阅读:157520 作者:2193

原文链接作者:Jakob Jenkov

译者:申章校对:丁一

有些情况下可以避免死锁。 本文展示了避免死锁的三种技术。

1 .锁定顺序

2 .锁定时间

3 .死锁检测

锁定顺序多个线程需要相同的锁定,但如果按不同的顺序锁定,则容易发生死锁。

如果可以确保所有线程都按相同的顺序锁定,则不会发生死锁。 请看以下示例:

如果thread 1: lockalockbthread 2: waitforalockc (whenalocked ) thread : waitforbwaitforc线程)需要锁定,则按固定顺序

例如,线程2和线程3仅在获取锁a后才尝试获取锁c (译者注:获取锁a是获取锁c所必需的)。 线程1已经有了锁a,所以线程2和3必须等待锁a被释放。 而且,在它们试图锁定b或c之前,必须成功地锁定a。

顺序加锁是一种有效的死锁预防机制。 但是,这种方法需要事先知道可能使用的所有锁,但有时总是无法预测。

解除锁定时间限制的另一种方法是在尝试获取锁定时添加超时时间。 这意味着在尝试获取锁定时,超过此时间限制的线程将丢弃锁定请求。 如果线程无法在指定时间内成功获得所有必需的锁定,则会执行回滚,并释放所有获得的锁定。 然后等待一段随机时间,然后重试。 这种随机等待时间允许其他线程尝试获取相同的锁,并在没有获取锁的情况下继续执行APP。 (译者注:锁定超时后,继续做其他事情,然后重复之前锁定的逻辑。

rgin-top:0px; margin-bottom:1em; padding-top:0px; padding-bottom:0px; line-height:35px; color:rgb(102,102,102); font-size:14px"> 以下是一个例子,展示了两个线程以不同的顺序尝试获取相同的两个锁,在发生超时后回退并重试的场景:

Thread 1 locks AThread 2 locks BThread 1 attempts to lock B but is blockedThread 2 attempts to lock A but is blockedThread 1's lock attempt on B times outThread 1 backs up and releases A as wellThread 1 waits randomly (e.g. 257 millis) before retrying.Thread 2's lock attempt on A times outThread 2 backs up and releases B as wellThread 2 waits randomly (e.g. 43 millis) before retrying.

在上面的例子中,线程2比线程1早200毫秒进行重试加锁,因此它可以先成功地获取到两个锁。这时,线程1尝试获取锁A并且处于等待状态。当线程2结束时,线程1也可以顺利的获得这两个锁(除非线程2或者其它线程在线程1成功获得两个锁之前又获得其中的一些锁)。

需要注意的是,由于存在锁的超时,所以我们不能认为这种场景就一定是出现了死锁。也可能是因为获得了锁的线程(导致其它线程超时)需要很长的时间去完成它的任务。

此外,如果有非常多的线程同一时间去竞争同一批资源,就算有超时和回退机制,还是可能会导致这些线程重复地尝试但却始终得不到锁。如果只有两个线程,并且重试的超时时间设定为0到500毫秒之间,这种现象可能不会发生,但是如果是10个或20个线程情况就不同了。因为这些线程等待相等的重试时间的概率就高的多(或者非常接近以至于会出现问题)。
(译者注:超时和重试机制是为了避免在同一时间出现的竞争,但是当线程很多时,其中两个或多个线程的超时时间一样或者接近的可能性就会很大,因此就算出现竞争而导致超时后,由于超时时间一样,它们又会同时开始重试,导致新一轮的竞争,带来了新的问题。)

这种机制存在一个问题,在Java中不能对synchronized同步块设置超时时间。你需要创建一个自定义锁,或使用Java5中java.util.concurrent包下的工具。写一个自定义锁类不复杂,但超出了本文的内容。后续的Java并发系列会涵盖自定义锁的内容。

死锁检测

死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。

每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。

当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。例如,线程A请求锁7,但是锁7这个时候被线程B持有,这时线程A就可以检查一下线程B是否已经请求了线程A当前所持有的锁。如果线程B确实有这样的请求,那么就是发生了死锁(线程A拥有锁1,请求锁7;线程B拥有锁7,请求锁1)。

当然,死锁一般要比两个线程互相持有对方的锁这种情况要复杂的多。线程A等待线程B,线程B等待线程C,线程C等待线程D,线程D又在等待线程A。线程A为了检测死锁,它需要递进地检测所有被B请求的锁。从线程B所请求的锁开始,线程A找到了线程C,然后又找到了线程D,发现线程D请求的锁被线程A自己持有着。这是它就知道发生了死锁。

下面是一幅关于四个线程(A,B,C和D)之间锁占有和请求的关系图。像这样的数据结构就可以被用来检测死锁。

那么当检测出死锁时,这些线程该做些什么呢?

一个可行的做法是释放所有锁,回退,并且等待一段随机的时间后重试。这个和简单的加锁超时类似,不一样的是只有死锁已经发生了才回退,而不会是因为加锁的请求超时了。虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁(编者注:原因同超时类似,不能从根本上减轻竞争)。

一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级。


扩展阅读:译者注

《现代操作系统 第三版》§6.2~§6.7

原创文章,转载请注明: 转载自并发编程网 – ifeve.com本文链接地址: 避免死锁


版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。