1. 什么是线程死锁
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去,如下图所示 在上图中,线程 A 已经持有了资源 2,它同时还想申请资源 1,线程 B 已经持有了资源 1,它同时还想申请资源 2,所以线程 1 和线程 2 就因为相互等待对方已经持有的资源,而进入了死锁状态。
2. 死锁产生的原因
那么为什么会产生死锁呢? 主要是由以下四个因素造成的:
- 互斥条件:指线程对以获取到的资源进行排他性使用,即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,则请求者只能只能等待,直到占有资源的线程释放该资源。
- 不可被剥夺条件:指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源。
- 请求并持有条件:值一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源。
- 环路等待条件:指在发生死锁时,必然存在一个(线程 — 资源)的环形链,即线程集合 {T0,T1,T2,…,Tn} 中的 T0 正在等待一个 T1 占用的资源,T1正在等待 T2 占用的资源,······Tn 正在等待已被 T0 占用的资源。
下面用一个例子来说明线程死锁:
public class ThreadDemo_线程死锁 {
public static void main(String[] args) {
Object lockA = new Object();
Object lockB = new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lockA) {
System.out.println("线程1 获得锁A");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1 等待锁B");
synchronized (lockB) {
System.out.println("线程1 获得锁B");
}
}
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lockB) {
System.out.println("线程2 获得锁B");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程2 等待锁A");
synchronized (lockA) {
System.out.println("线程2 获得锁A");
}
}
}
});
t2.start();
}
}
通过C:\Program Files\Java\jdk1.8.0_192\bin 下的 VisualVM 就可以观察到我们启动的线程检测到了死锁,导致无法继续运行也无法结束进程,进入死锁状态。 输出如下结果:
分析代码结果:Thread-0 是线程1 ,Thread-1 是线程 2,代码首先创建了两个资源,并创建了两个线程。从结果输出可以看出,线程调度器先调度了线程 1,也就是把 CPU 资源分配给了线程 A,线程 A 使用 synchronized(lockA) 方法获得到了 lockA 的监视器锁,然后调用 sleep 函数休眠 1s,休眠 1s 是为了保证线程 1 在获取 lockB 对应的锁前让 线程 2 抢占到 CPU,获取到资源 lockB 对象的监视器锁资源,然后调用 sleep 函数休眠 1s。
好了,到了这里线程 1 获取到了 lockA 资源,线程 2 获取到了 lockB 资源。线程 1 休眠结束后会企图获取 lockB 资源,而 lockB 资源被线程 2 所持有,所以线程 1 会阻塞而等待。而同时线程 2 休眠结束后会企图获取 lockA 资源,而 lockA 资源已经被线程 1 所持有,**所以线程 1 和线程 2 就陷入了相互等待的状态,也就产生了死锁。**下面谈谈本例是如何满足死锁的四个条件的。
首先,lockA 和 lockB 都是互斥资源,当线程 1 调用了 synchronized(lockA) 方法获得到 lockA 上的监视器并释放前,线程 2 再调用 synchronized(lockA) 方法尝试获取该资源会被阻塞,只有线程 1 主动释放该锁,线程 2 才能获得,这满足了资源互斥条件。
线程 1 首先通过 synchronized(lockA) 方法获取到 lockA 上的监视器锁资源,然后通过 synchronized(lockB) 方法等待获取 lockB 上的监视器锁资源,这就构成了请求并持有条件。
线程 1 在获取 lockA 上的监视器锁资源后,该线程不会被线程 2 掠夺走,只有线程 1 自己主动释放 lockA 资源时,他才会放弃对该资源的持有权,这构成了资源不可剥夺条件。
线程 1 持有 lockA 资源并等待获取 lockB 资源,而线程 2 只有 lockB 资源并等待获取 lockA 资源,这构成了环路等待条件。所以线程 1 和线程 2 就进入了死锁状态。
3. 如何避免线程死锁。
要想避免死锁,只需要破坏掉至少一个构造死锁的必要条件即可,而在操作系统中,互斥条件和不可剥夺条件是系统规定的,这也没办法人为更改,而且这两个条件很明显是一个标准的程序应该所具备的特性。所以目前只有请求并持有和环路等待条件是可以被破坏的。
造成死锁的原因其实和申请资源的顺序有很大关系 使用资源申请的有序性原则就可以避免死锁,那么什么是资源申请的有序性呢?我们对上面线程 2 的代码进行如下修改。
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lockA) {
System.out.println("线程2 获得锁A");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程2 等待锁B");
synchronized (lockB) {
System.out.println("线程2 获得锁B");
}
}
}
});
t2.start();
}
输出结果如下: 如上代码让在线程 2 中获取资源的顺序和在线程 1 中获取资源的顺序保持一致,其实资源分配有序性就是指,假如线程 1 和线程 2 都需要资源 1, 2, 3, … . , ,对资源进行排序,线程 1 和线程 2 有在获取了资源 n-1 时才能去获取资源 n。
我们可以简单分析一下为何资源的有序分配会避免死锁,比如上面的代码,假如线程1 和线程 2 同时执行到了 synchronized(lockA),只有1个线程可以获取到 lockA 上的监视器锁,假如线程 1 获取到了,那么线程 2 就会被阻塞而不会再去获取资源 B,线程 1 获取 lockA 的监视器锁后会去申请 lockB 的监视器锁资源,这时候线程 1 是可以获取到的,线程 1 获取到 lockB 资源并使用后会放弃对资源 lockB 的持有,然后再释放对 lockA 的持有,释放 lockA 后线程 2 才会被从阻塞状态变为激活状态。所以资源的有序性破坏了资源的请求并持有条件和环路等待条件,因此避免了死锁。
|