防止死锁

时间:2020-01-09 10:35:51  来源:igfitidea点击:

在某些情况下,可以防止死锁。我将在本文中描述三种技术:

  • 锁顺序
  • 锁定超时
  • 死锁检测

锁顺序

当多个线程需要相同的锁但以不同的顺序获得它们时,就会发生死锁。

如果确保任何线程始终以相同的顺序获取所有锁,则不会发生死锁。看这个例子:

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)需要多个锁,则必须按确定的顺序进行操作。在获得较早的锁定之前,它无法在序列的后面获得锁定。

例如,线程2或者线程3都不能锁定C,直到它们先锁定A。由于线程1持有锁A,因此线程2和3必须首先等待直到锁A解锁。然后,他们必须成功锁定A,然后才能尝试锁定B或者C。

锁排序是一种简单而有效的防止死锁机制。但是,仅当我们在获取任何锁之前了解所有所需的锁时,才可以使用它。这并非总是如此。

锁定超时

另一个防止死锁的机制是对锁定尝试设置超时,这意味着试图获得锁定的线程只会尝试放弃这么长时间。如果线程在给定的超时时间内未成功获取所有必要的锁,它将备份,释放所有已获取的锁,等待随机的时间,然后重试。等待的随机时间使尝试采用相同锁定的其他线程有机会获得所有锁定,从而使应用程序可以继续运行而不会锁定。

这是两个线程尝试以不同顺序获取相同的两个锁的示例,其中线程备份并重试:

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或者另一个线程在两者之间获取锁)。

要记住的一个问题是,仅由于锁定超时并不一定意味着线程已死锁。这也可能仅意味着持有锁的线程(导致另一个线程超时)需要很长时间才能完成其任务。

此外,如果有足够的线程争用相同的资源,即使超时和备份,它们仍然有冒险尝试一次又一次地占用线程。在重试之前,有2个线程在0到500毫秒之间等待,可能不会发生这种情况,但是有10或者20个线程时,情况就不同了。然后,两个线程在重试之前(或者足够接近以引起问题)同时等待的可能性要高得多。

锁定超时机制的问题在于,无法为Java中的同步块设置超时设置。我们将必须创建一个自定义锁类或者使用java.util.concurrency包中的Java 5并发构造之一。编写自定义锁并不困难,但这不在本文讨论范围之内。 Java并发跟踪中的后续文本将涵盖自定义锁。

死锁检测

死锁检测是一种较重的防止死锁机制,用于无法进行锁排序和锁超时不可行的情况。

线程每次获取锁时,都会在线程和锁的数据结构(映射,图形等)中进行记录。此外,每当线程请求锁定时,此数据结构中也会对此进行记录。

当线程请求锁定但请求被拒绝时,线程可以遍历锁定图以检查死锁。例如,如果线程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本身持有的一个锁。然后,它知道发生了死锁。

如果检测到死锁,线程将如何处理?

一种可能的操作是释放所有锁,备份,等待随机的时间,然后重试。这类似于更简单的锁定超时机制,除了线程仅在实际发生死锁时才进行备份。不仅仅是因为他们的锁定请求超时。但是,如果许多线程在争夺相同的锁,则即使它们备份并等待,它们也可能反复陷入死锁。

更好的选择是确定或者分配线程的优先级,以便仅备份一个(或者几个)线程。其余线程继续获取所需的锁,就好像没有发生死锁一样。如果分配给线程的优先级是固定的,则相同的线程将始终被赋予更高的优先级。为避免这种情况,我们可以在检测到死锁时随机分配优先级。