什么是线程死锁
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去,如图 1-2 所示。
图 1-2
在图 1-2 中,线程 A 已经持有了资源 2,它同时还想申请资源 1,线程 B 已经持有了资源 1,它同时还想申请资源 2,所以线程 1 和线程 2 就因为相互等待对方已经持有的资源,而进入了死锁状态。
那么为什么会产生死锁呢?学过操作系统的朋友应该都知道,死锁的产生必须具备以下四个条件。
● 互斥条件:指线程对已经获取到的资源进行排它性使用,即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。
● 请求并持有条件:指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源。
● 不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源。
● 环路等待条件:指在发生死锁时,必然存在一个线程—资源的环形链,即线程集合{T0,T1,T2,…,Tn}中的 T0 正在等待一个 T1 占用的资源,T1 正在等待 T2 占用的资源,……Tn正在等待已被 T0 占用的资源。
下面通过一个例子来说明线程死锁。
public class DeadLockTest2 {
// 创建资源
private static Object resourceA = new Object();
private static Object resourceB = new Object();
public static void main(String[] args) {
// 创建线程 A
Thread threadA = new Thread(new Runnable() {
public void run() {
synchronized (resourceA) {
System.out.println(Thread.currentThread() + 「 get ResourceA」);
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 esourceB」);
}
}
}
});
// 创建线程 B
Thread threadB = new Thread(new Runnable() {
public void run() {
synchronized (resourceB) {
System.out.println(Thread.currentThread() + 「 get ResourceB」);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + 「waiting get esourceA」);
synchronized (resourceA) {
System.out.println(Thread.currentThread() + 「get ResourceA」);
}
};
}
});
// 启动线程
threadA.start();
threadB.start();
}
}
输出结果如下。
下面分析代码和结果:Thread-0 是线程 A,Thread-1 是线程 B,代码首先创建了两个资源,并创建了两个线程。从输出结果可以知道,线程调度器先调度了线程 A,也就是把 CPU 资源分配给了线程 A,线程 A 使用 synchronized(resourceA)方法获取到了 resourceA 的监视器锁,然后调用 sleep 函数休眠 1s,休眠 1s 是为了保证线程 A 在获取 resourceB 对应的锁前让线程 B 抢占到 CPU,获取到资源 resourceB 上的锁。线程 A 调用 sleep 方法后线程 B 会执行 synchronized(resourceB)方法,这代表线程 B 获取到了 resourceB 对象的监视器锁资源,然后调用 sleep 函数休眠 1s。好了,到了这里线程 A 获取到了 resourceA 资源,线程 B 获取到了 resourceB 资源。线程 A 休眠结束后会企图获取 resourceB 资源,而 resourceB 资源被线程 B 所持有,所以线程 A 会被阻塞而等待。而同时线程 B 休眠结束后会企图获取 resourceA 资源,而 resourceA 资源已经被线程 A 持有,所以线程 A 和线程 B 就陷入了相互等待的状态,也就产生了死锁。下面谈谈本例是如何满足死锁的四个条件的。
首先,resourceA 和 resourceB 都是互斥资源,当线程 A 调用 synchronized(resourceA)方法获取到 resourceA 上的监视器锁并释放前,线程 B 再调用 synchronized(resourceA)方法尝试获取该资源会被阻塞,只有线程 A 主动释放该锁,线程 B 才能获得,这满足了资源互斥条件。
线程 A 首先通过 synchronized(resourceA)方法获取到 resourceA 上的监视器锁资源,然后通过 synchronized(resourceB)方法等待获取 resourceB 上的监视器锁资源,这就构成了请求并持有条件。
线程 A 在获取 resourceA 上的监视器锁资源后,该资源不会被线程 B 掠夺走,只有线程 A 自己主动释放 resourceA 资源时,它才会放弃对该资源的持有权,这构成了资源的不可剥夺条件。
线程 A 持有 objectA 资源并等待获取 objectB 资源,而线程 B 持有 objectB 资源并等待 objectA 资源,这构成了环路等待条件。所以线程 A 和线程 B 就进入了死锁状态。
如何避免线程死锁
要想避免死锁,只需要破坏掉至少一个构造死锁的必要条件即可,但是学过操作系统的读者应该都知道,目前只有请求并持有和环路等待条件是可以被破坏的。
造成死锁的原因其实和申请资源的顺序有很大关系,使用资源申请的有序性原则就可以避免死锁,那么什么是资源申请的有序性呢?我们对上面线程 B 的代码进行如下修改。
// 创建线程 B
Thread threadB = new Thread(new Runnable() {
public void run() {
synchronized (resourceA) {
System.out.println(Thread.currentThread() + 「 get ResourceB」);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + 「waiting get ResourceA」);
synchronized (resourceB) {
System.out.println(Thread.currentThread() + 「get ResourceA」);
}
};
}
});
输出结果如下。
如上代码让在线程 B 中获取资源的顺序和在线程 A 中获取资源的顺序保持一致,其实资源分配有序性就是指,假如线程 A 和线程 B 都需要资源 1,2,3,…,n时,对资源进行排序,线程 A 和线程 B 只有在获取了资源n-_1 时才能去获取资源_n。
我们可以简单分析一下为何资源的有序分配会避免死锁,比如上面的代码,假如线程 A 和线程 B 同时执行到了 synchronized(resourceA),只有一个线程可以获取到 resourceA 上的监视器锁,假如线程 A 获取到了,那么线程 B 就会被阻塞而不会再去获取资源 B,线程 A 获取到 resourceA 的监视器锁后会去申请 resourceB 的监视器锁资源,这时候线程 A 是可以获取到的,线程 A 获取到 resourceB 资源并使用后会放弃对资源 resourceB 的持有,然后再释放对 resourceA 的持有,释放 resourceA 后线程 B 才会被从阻塞状态变为激活状态。所以资源的有序性破坏了资源的请求并持有条件和环路等待条件,因此避免了死锁。