在一些场景中,去预防死锁是可能的。在这个内容中我将会描述三种技术:
- 锁排序
- 锁超时
- 死锁检测
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,需要几个锁,它就必须在固定的顺序获取他们。它就不能在之后的序列中获取一个锁直到已经获取到更早的锁。
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将会重试获取这个锁大概200毫秒在线程1之前,并且很可能的成功获取这两个锁。 已经尝试获取锁A的线程1然后等待。当线程2结束后,线程1将会能够获取这两个锁(除非线程1或者其他的线程在这个期间获取这个锁了)。
另外,如果足够多的线程竞争相同的资源,他们将会冒险尝试一次又一次的同时获取这个线程,甚至如果超时以及回退了。在重试之前有两个线程每一个等待时间在0到500毫秒之间的话,这个可能不会发生,但是如果10或者20个线程这个情况就不同了。然后在重试之前两个线程等待相同时间的可能性就会高很多了。
伴随着锁超时的一个问题就是,在java中不可能在同步代码块中设置一个超时时间。你将会不得不创建一个自定义的锁类或者使用java5中并发包中的一个。写自定义锁不难,但是超出了这篇文章的范围。在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)。一个数据结构像这样就可以用来检测死锁。
那么如果一个死锁被检测到了,这个线程会做什么呢?
一个可能的动作就是去释放所有的锁,回退,等待一个随机数量的时间然后重试。这个跟更简单的锁超时机制更加相似除了当一个死锁发生的时候线程只是回退。不只是因为他们的锁请求超时。然而,如果许多的线程正在竞争相同的锁,他们可能重复的以死锁结束,甚至如果他们回退并且等待。
一个更好的选项就是决定或者分配一个线程的优先级以至于只有一个(或者一些)线程回退。剩下的线程继续获取他们需要的锁因为没有死锁发生的话。如果被分配到线程的优先级被固定的话,相同的线程将会总是被分配更高的优先级。为了避免这个,你可能随机的分配优先级无论死锁什么时间被检测到。
翻译地址:http://tutorials.jenkov.com/java-concurrency/deadlock-prevention.html