1. 什么是死锁?
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成互相等待的现象,在无外力作用的情况下,这些线程会相互等待而无法继续运行下去。
线程T1已经持有了资源R1,他同时还想申请资源R2,与此同时,线程T2已经持有了资源R2,同时还想申请资源R1,所以线程T1和T2就因为相互等待对方已经持有的资源,而进入了死锁状态。
2. 死锁产生的必要条件
产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生。
互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, …, pn},其中Pi等 待的资源被P(i+1)占有(i=0, 1, …, n-1),Pn等待的资源被P0占有
下面来通过一个例子来说明线程死锁
public class StringTest {
private static Object resourceA = new Object();
private static Object resourceB = new Object();
public static void main(String[] args) {
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceA){
System.out.println(Thread.currentThread() + "get RescourceA");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get sourceB");
synchronized (resourceB){
System.out.println(Thread.currentThread() + "get RescourceB");
}
}
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceB){
System.out.println(Thread.currentThread() + "get RescourceB");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get sourceA");
synchronized (resourceA){
System.out.println(Thread.currentThread() + "get RescourceA");
}
}
}
});
threadA.start();
threadB.start();
}
}
让我们来分析一下它是如何满足死锁的四个条件的
首先,resourceA和resourceB都是互斥资源,当线程A调用 synchronized (resourceA)方法获取到resourceA上的监视器锁并释放前,线程B在调用 synchronized (resourceA)方法尝试获取该资源会被阻塞,只有线程A主动释放该锁,线程B才能获得,这满足了资源互斥条件。
线程A首先通过 synchronized (resourceA)方法获取到resourceA上的监视器锁资源,然后通过synchronized (resourceB)方法等待获取resourceB上监视器锁资源, 这就构成了请求并持有条件。
线程A在获取resourceA上的监视器资源后,该资源不会被线程B掠夺走,只有线程A自己主动释放resourceA资源时, 它才会放弃对该资源的持有权,这构成了资源的不可剥夺条件。
线程A持有资源A并等待获取资源B,而线程B持有资源B并等待获取资源A,这构成了环路等待条件。
所以线程A和线程B就进入了死锁状态。
3. 如何避免死锁
想要避免死锁,只需要破坏掉至少一个构造死锁的必要条件即可,但是学过操作系统的都应该知道,目前只有请求并持有和环路等待条件是可以被破坏的。
造成死锁的原因其实和申请资源的顺序有很大的关系,使用资源申请的有序性原则就可以避免死锁。而资源分配有序性就是指,假如多个线程都需要资源1,2,3,.。。。,n时,对资源进行排序,线程只有获取了资源N-1时才能去获取资源N。因为资源的有序性破坏了资源的请求并持有条件和环路等待条件,因此避免了死锁。
4. 死锁检测
死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。
每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。
当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。例如,线程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自己持有着。这是它就知道发生了死锁。