目录
死锁是什么
概述
死锁是指两个或多个进程(或线程)彼此互相占用对方需要的资源而陷入一种无限等待的状态。当多个进程同时请求互斥的资源时,如果每个进程都持有至少一项资源并等待其他进程释放其资源,那么这些进程就会形成死锁。
假设进程 A 持有资源 b,同时需要资源 a,而进程 B 则持有资源 b,同时需要资源 a,这时候如果进程 A 和进程 B 都不释放它们已经占有的资源,那么它们就会陷入无限等待的状态,从而形成死锁。这样的话,这两个进程都无法继续执行,也无法释放资源,从而导致整个系统无法运转。可以对应下面的图进行理解
死锁是一种非常危险的BUG,因为它可能导致系统崩溃或无法正常工作.因此,在设计系统时需要采取措施来预防死锁的发生,例如采用死锁避免等方法.
图解
以下这种顺序两个线程同时来执行的话就会产生死锁
这样谁都不释放锁,就成了僵局,图中没写全
这里可以引出一个小故事(早起吃牛大然后死锁的故事)
有一天早上我去吃牛肉面了,当时刚要了个二细端上,发现没有了空桌子,只能拼桌坐了,我坐下之后拿起醋加了点,不小心给到到旁边桌子上了,我对面做的一个人正好拿的纸在那擦桌子,我说兄弟给点纸,但是他脾气有点大,噘着嘴说:醋给我我给你给纸,我也没睡醒有点起床气,我说纸给我我给你给醋,然后我两谁也不让谁,然后就陷入了僵局,(死锁了),但是如果我两一开始都先加醋然后用纸,按照这个顺序就不会有这样的事情发生就不会死锁了(这也就是下面讲的破坏环路等待)
死锁产生的四个必要条件:
概述
-
互斥使用: 即当资源被一个线程使用(占有)时,别的线程不能使用
-
不可抢占: 资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
-
请求和保持: 即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
-
循环等待: 即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路
当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失
-
其中最容易破坏的就是"循环等待"
避免死锁的方法
概述
-
避免使用多个资源:减少系统中资源的数量可以减少死锁的发生概率。
-
避免持有资源后请求其他资源:当进程持有某个资源时,尽量不要再请求其他资源,可以先释放已经持有的资源再请求其他资源。
-
破坏循环等待:对系统中的资源进行编号,强制要求进程按照编号的顺序请求资源,从而避免循环等待的情况。
-
资源剥夺:在某些情况下,可以强制剥夺某个进程持有的资源,从而避免死锁的发生。
-
超时机制:为每个请求资源的操作设置一个超时时间,在超时时间内如果不能得到所需资源就放弃请求并释放已经持有的资源,从而避免死锁的发生。
破坏循环等待
概述
最常用的一种死锁阻止技术就是锁排序. 假设有 N 个线程尝试获取 M 把锁, 就可以针对 M 把锁进行编号(1, 2, 3...M). N 个线程尝试获取锁的时候, 都按照固定的按编号由小到大顺序来获取锁. 这样就可以避免环路等待.
两个线程对于加锁的顺序没有约定,就容易产生环路等待
代码示例
Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread() {
@Override
public void run() {
synchronized (lock1) {
synchronized (lock2) {
}
}
}
};
t1.start();
Thread t2 = new Thread() {
@Override
public void run() {
synchronized (lock2) {
synchronized (lock1) {
}
}
}
};
t2.start();
不会产生环路等待的代码
约定好先获取 lock1, 再获取 lock2 , 就不会环路等待
Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread() {
@Override
public void run() {
synchronized (lock1) {
synchronized (lock2) {
}
}
}
};
t1.start();
Thread t2 = new Thread() {
@Override
public void run() {
synchronized (lock1) {
synchronized (lock2) {
}
}
}
);
t2.start