Java concurrency《防止死锁》
Java concurrency《防止死锁》
常见预防死锁的办法
- 有顺序的锁
- 具有超时时间的锁
- 死锁的检测
有顺序的锁
当多个线程需要相同的锁定但以不同的顺序获取时,会发生死锁。
如果您确保所有锁总是以任何线程以相同的顺序执行,则不会发生死锁。 看这个例子:
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,从中找到线程A本身持有的锁定之一。那么它知道发生了死锁。
以下是4个线程(A,B,C和D)采取和请求的锁图。这样的数据结构可以用来检测死锁。
那么如果检测到死锁,线程会做什么呢?
一个可能的操作是释放所有的锁,备份,等待随机的时间,然后重试。 这类似于更简单的锁定超时机制,除了线程仅在实际发生死锁时进行备份。 不仅因为他们的锁请求超时。 然而,如果许多线程正在竞争相同的锁,那么即使他们备份并等待,它们也可能会重复地导致死锁。
更好的选择是确定或分配线程的优先级,以便只有一个(或几个)线程备份。 线程的其余部分继续采取他们需要的锁,就像没有发生死锁一样。 如果分配给线程的优先级是固定的,那么相同的线程总是被赋予更高的优先级。 为了避免这种情况,您可以在检测到死锁时随机分配优先级。