如何避免死锁?死锁避免的几种方式

TheDisguiser 2020-04-15 17:31:23 java常见问答 8096

在Java项目开发中,有时候会出现死锁这种玩意,它会让你的程序不能动弹,是个非常讨厌的家伙,下面我们就来讲讲死锁避免的几种方式,一起来了解一下吧。

死锁是指两个或以上的线程在执行过程中,互相竞争资源或彼此通信造成的一种阻塞现象,无外力作用下,它们永远无法推进。这时我们一般就会说系统已经处于死锁状态或者系统产生了死锁。这种一直在互相等待的进程被称为死锁进程。下面我们就来了解一下三种避免死锁的方式吧。

一、加锁顺序

当多个线程同时需要一样的锁,但是要按照不同的顺序加锁,就会容易发生死锁。

如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。看下面这个例子:

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

如果一个线程需要一些锁,那么它必须按照确定的顺序获取锁。它只有获得了从顺序上排在前面的锁之后,才能获取后锁。

例如,线程2和线程3只有在获取了锁A之后才能尝试获取锁C,获取锁A是获取锁C的必要条件。因为线程1已经拥有了锁A,所以线程2和3需要一直等到锁A被释放。然后在它们尝试对B或C加锁之前,必须成功地对A加了锁。

按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需要你事先知道所有可能会用到的锁(译者注:并对这些锁做适当的排序),但总有些时候是无法预知的。

二、加锁时限

另外一个可以避免死锁的方法是在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行,加锁超时后可以先继续运行干点其它事情,再回头来重复之前加锁的逻辑。

以下是一个例子,展示了两个线程以不同的顺序尝试获取相同的两个锁,在发生超时后回退并重试的场景:

Thread 1 locks A
Thread 2 locks B
Thread 1 attempts to lock B but is blocked
Thread 2 attempts to lock A but is blocked
Thread 1 's lock attempt on B times out
Thread 1 backs up and releases A as well
Thread 1 waits randomly(e.g.257 millis) before retrying.
Thread 2 's lock attempt on A times out
Thread 2 backs up and releases B as well
Thread 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并发系列会涵盖自定义锁的内容。

三、死锁检测

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

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

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

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

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

如何避免死锁

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

一个可行的做法是释放所有锁,回退,并且等待一段随机的时间后重试。这种方法与简单的加锁超时类似,不同的是只有死锁发生了它才会回退,而不是因为加锁请求超时了。虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复形成死锁。

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

以上就是今天关于避免死锁的所有内容啦,想知道更多的Java一些知识问答小知识的话,就请多多关注本站吧。