本节内容主要是对死锁进行深入的讲解,具体内容点如下:
概述:在多线程编程中,线程个数一般都大于 CPU 个数,而每个 CPU 同一时-刻只能被一个线程使用,为了让用户感觉多个线程是在同时执行的, CPU 资源的分配采用了时间片轮转的策略,也就是给每个线程分配一个时间片,线程在时间片内占用 CPU 执行任务。
定义:当前线程使用完时间片后,就会处于就绪状态并让出 CPU,让其他线程占用,这就是上下文切换,从当前线程的上下文切换到了其他线程。
问题点解析:那么就有一个问题,让出 CPU 的线程等下次轮到自己占有 CPU 时如何知道自己之前运行到哪里了?所以在切换线程上下文时需要保存当前线程的执行现场, 当再次执行时根据保存的执行现场信息恢复执行现场。
线程上下文切换时机: 当前线程的 CPU 时间片使用完或者是当前线程被其他线程中断时,当前线程就会释放执行权。那么此时执行权就会被切换给其他的线程进行任务的执行,一个线程释放,另外一个线程获取,就是我们所说的上下文切换时机。
定义:死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。
如上图所示死锁状态,线程 A 己经持有了资源 2,它同时还想申请资源 1,可是此时线程 B 已经持有了资源 1 ,线程 A 只能等待。
反观线程 B 持有了资源 1 ,它同时还想申请资源 2,但是资源 2 已经被线程 A 持有,线程 B 只能等待。所以线程 A 和线程 B 就因为相互等待对方已经持有的资源,而进入了死锁状态。
如下图所示:
为了更好的了解死锁是如何产生的,我们首先来设计一个死锁争夺资源的场景。
场景设计:
期望结果:发生死锁,线程 threadA 和 threadB 互相等待。
Tips:此处的实验会使用到关键字 synchronized,后续小节还会对关键字 synchronized 单独进行深入讲解,此处对 synchronized 的使用仅仅为初级使用,有 JavaSE 基础即可。
实例:
public class DemoTest{
private static Object resourceA = new Object();//创建资源 resourceA
private static Object resourceB = new Object();//创建资源 resourceB
public static void main(String[] args) throws InterruptedException {
//创建线程 threadA
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceA) {
System.out.println(Thread.currentThread().getName() + "获取 resourceA。");
try {
Thread.sleep(1000); // sleep 1000 毫秒,确保此时 resourceB 已经进入run 方法的同步模块
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "开始申请 resourceB。");
synchronized (resourceB) {
System.out.println (Thread.currentThread().getName() + "获取 resourceB。");
}
}
}
});
threadA.setName("threadA");
//创建线程 threadB
Thread threadB = new Thread(new Runnable() { //创建线程 1
@Override
public void run() {
synchronized (resourceB) {
System.out.println(Thread.currentThread().getName() + "获取 resourceB。");
try {
Thread.sleep(1000); // sleep 1000 毫秒,确保此时 resourceA 已经进入run 方法的同步模块
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "开始申请 resourceA。");
synchronized (resourceA) {
System.out.println (Thread.currentThread().getName() + "获取 resourceA。");
}
}
}
});
threadB.setName("threadB");
threadA. start();
threadB. start();
}
}
代码讲解:
执行结果验证:
threadA 获取 resourceA。
threadB 获取 resourceB。
threadA 开始申请 resourceB。
threadB 开始申请 resourceA。
看下验证结果,发现已经出现死锁,threadA 申请 resourceB,threadB 申请 resourceA,但均无法申请成功,死锁得以实验成功。
要想避免死锁,只需要破坏掉至少一个构造死锁的必要条件即可,学过操作系统的读者应该都知道,目前只有请求并持有和环路等待条件是可以被破坏的。
造成死锁的原因其实和申请资源的顺序有很大关系,使用资源申请的有序性原则就可避免死锁。
我们依然以第 5 个知识点进行讲解,那么实验的需求和场景不变,我们仅仅对之前的 threadB 的代码做如下修改,以避免死锁。
代码修改:
Thread threadB = new Thread(new Runnable() { //创建线程 1
@Override
public void run() {
synchronized (resourceA) { //修改点 1
System.out.println(Thread.currentThread().getName() + "获取 resourceB。");//修改点 3
try {
Thread.sleep(1000); // sleep 1000 毫秒,确保此时 resourceA 已经进入run 方法的同步模块
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "开始申请 resourceA。");//修改点 4
synchronized (resourceB) { //修改点 2
System.out.println (Thread.currentThread().getName() + "获取 resourceA。"); //修改点 5
}
}
}
});
请看如上代码示例,有 5 个修改点:
请读者按指示修改代码,并从新运行验证。
修改后代码讲解:
执行结果验证:
threadA 获取 resourceA。
threadA 开始申请 resourceB。
threadA 获取 resourceB。
threadB 获取 resourceA。
threadB 开始申请 resourceB。
threadB 获取 resourceB。
我们发现 threadA 和 threadB 按照相同的顺序对 resourceA 和 resourceB 依次进行访问,避免了互相交叉持有等待的状态,避免了死锁的发生。
死锁是并发编程中最致命的问题,如何避免死锁,是并发编程中恒久不变的问题。
掌握死锁的实现以及如果避免死锁的发生,是本节内容的重中之重。
0/1000