死锁
《深入理解计算机系统》对死锁的定义如下:死锁(基于信号量机制)指的是一组线程被阻塞了,等待一个永远不为真的条件。
这样的概念相信睿智的你也不能吃透它。
因此笔者试图在Google上搜索关于死锁的定义,其中感觉维基百科中对死锁的定义十分严谨。
In concurrent computing, a deadlock is a state in which each member of a group of actions, is waiting for some other member to release a lock.
In an operating system, a deadlock occurs when a process or thread enters a waiting state because a requested system resource is held by another waiting process, which in turn is waiting for another resource held by another waiting process.
其中说明:死锁是一种状态、一个僵局,是一组活动中的任一活动都在等待其他活动释放锁的状态。这个活动可以代指进程或者线程。
死锁产生的原因
1、系统资源的限制(比如内存,IO)
例:假如现在有两个进程,每个进程都需要1.5GB的内存才能够正常运行,不幸的是机器只有2GB的内存(没有虚拟内存)。这时候操作系统只能给每个进程1GB的内存,这两个进程都不能正常运行直到其中一个进程放弃一些资源。
2、程序的推进顺序不当
进程或者线程请求和释放资源的顺序不当,也同样会导致死锁。
例如,并发进程 P1、P2分别保持了资源R1、R2,而进程P1申请资源R2,进程P2申请资源R1时,两者都 会因为所需资源被占用而阻塞。
信号量使用不当也会造成死锁。进程间彼此相互等待对方发来的消息,结果也会使得这 些进程间无法继续向前推进。
例如,进程A等待进程B发的消息,进程B又在等待进程A 发的消息,可以看出进程A和B不是因为竞争同一资源,而是在等待对方的资源导致死锁。
死锁的必要条件
在并发编程中,死锁的确是一种潜在的令人厌恶的运行时错误。但是要想产生死锁也并非易事,必须满足Coffman Conditions。
- Mutual exclusion. There must be some resource that can’t be shared between processes.
- Hold and wait. Some process must be holding one resource while waiting for another.
- No preemption. Only the process holding a resource can release it.
- Circular wait. There must be some cycle of waiting processes P1, P2, …, Pn such that each process Pi is waiting for a resource held by the next process in the cycle.
以上即为Coffman Conditions。也就是人们常说的死锁的四个必要条件:
- 互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
- 保持等待条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不可剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
- 环路等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, …, pn},其中Pi等 待的资源被P(i+1)占有(i=0, 1, …, n-1),Pn等待的资源被P0占有。如下图所示:
如何阻止死锁
我们可以通过违反死锁的一个或多个必要条件来阻止死锁,这也是最常见的解决方式。
- 破坏互斥条件:没有互斥,就不会产生死锁,这是最佳的解决方案,特别是共享资源不可变时(如只读文件,无锁数据结构)。但是对可变资源(大多数可写的数据结构)就会产生不可预料的结果。
破坏保持等待条件
采用原子思想来完成,得到全部资源的进程才可运行,否则就释放其占据的资源。Adopt a policy of not letting a process wait for one resource while holding another, either by requiring each process to hold only one resource at a time, or to request all of the resources.
破坏不可剥夺条件
可以给进程设置抢占资源的优先级来破坏该条件。具体请参考下文:Allow preemption. This is almost as good as eliminating mutual exclusion: if I can bump some process off a resource it is hogging, then I can break a deadlock cycle. The CPU is the primary preemptible resource (although we may get deadlock if we allow processes to block preemption while waiting for other resources—as can happen with disabled interrupts or in cases of priority inversion where a high-priority process pushes the low-priority process it’s waiting for off the CPU). Another preemptible resource is memory (assuming we can swap out to disk). The difference between preemption and sharing is that in the preemption case we just need to be able to restore the state of the resource for the preempted process rather than letting it in at the same time as the preemptor.
破坏环路等待条件
破坏环路等待的方式有很多:
如:当资源请求形成环路时,可以将一个进程阻塞,释放其占据的资源,以使得其他的进程顺利完成,最后再执行该进程。如下图所示:
死锁的其他解决方式:
Ostrich algorithm 鸵鸟算法(当你对某一件事情没有一个很好的解决方法时,那就忽略它,就像鸵鸟面对危险时会把它深埋在沙砾中,装作看不到。该算法可以应用在极少发生死锁的的情况下。)
Banker’s algorithm 银行家算法( 所谓银行家算法,是指在分配资源之前先看清楚,资源分配后是否会导致系统死锁。如果会死锁,则不分配,否则就分配)
这里也不是一句两句就能阐述清楚的,有兴趣的读者可以找度娘问问。
模拟死锁
笔者采用Java线程的方式来模拟死锁。
情景:有A,B两个对象,线程1锁住了A,然后尝试对B进行加锁,同时线程2已经锁住了B,接着尝试对A进行加锁。
这时死锁就发生了。线程1永远得不到B,线程2也永远得不到A。为了得到彼此的对象(A和B),它们将永远阻塞下去。
代码如下:
public class T {
Object A = new Object();
Object B = new Object();
void m1(){
System.out.println(Thread.currentThread().getName() + " 锁住A对象");
synchronized (A) {
//主要起放大的作用,让线程2获得处理器资源
//如果不加的话,线程1很有可能就直接锁定了A和B,那么线程2就直接处于阻塞,不会引起资源争用问题,死锁也就不会产生
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (B) {
System.out.println(Thread.currentThread().getName() + " 锁住B对象");
}
}
}
void m2(){
System.out.println(Thread.currentThread().getName() + " 锁住B对象");
synchronized (B) {
synchronized (A) {
System.out.println(Thread.currentThread().getName() + " 锁住A对象");
}
}
}
public static void main(String[] args) {
T t = new T();
new Thread(() -> t.m1(),"Thread 1").start();
new Thread(() -> t.m2(),"Thread 2").start();
}
}
避免死锁
- 加锁顺序(线程按照一定的顺序加锁)
- 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
死锁检测
关于避免死锁的
具体可以参见(http://blog.csdn.net/ls5718/article/details/51896159)
本文参考:
https://en.wikipedia.org/wiki/Deadlock
http://www.cs.yale.edu/homes/aspnes/pinewiki/Deadlock.html
http://ifeve.com/deadlock/