理解线程的上下文切换
概述:在多线程编程中,线程个数一般都大于 CPU 个数,而每个 CPU 同一时-刻只能被一个线程使用,为了让用户感觉多个线程是在同时执行的, CPU 资源的分配采用了时间片轮转的策略,也就是给每个线程分配一个时间片,线程在时间片内占用 CPU 执行任务。
定义:当前线程使用完时间片后,就会处于就绪状态并让出 CPU,让其他线程占用,这就是上下文切换,从当前线程的切换到了其他线程。
线程上下文切换时机: 当前线程的 CPU 时间片使用完或者是当前线程被其他线程中断时,当前线程就会释放执行权。那么此时执行权就会被切换给其他的线程进行任务的执行,一个线程释放,另外一个线程获取,就是我们所说的上下文切换时机。
什么是线程死锁
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。
- 如上图所示死锁状态,线程 A 己经持有了资源 2,它同时还想申请资源 1,可是此时线程 B 已经持有了资源 1 ,线程 A 只能等待。
- 反观线程 B 持有了资源 1 ,它同时还想申请资源 2,但是资源 2 已经被线程 A 持有,线程 B 只能等待。所以线程 A 和线程 B 就因为相互等待对方已经持有的资源,而进入了死锁状态。
线程死锁的必备要素
- 互斥条件:进程要求对所分配的资源进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待;
两个线程拥有互斥的资源而且都没有释放。
- 不可剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放,如 yield 释放 CPU 执行权);
没有外力强迫其释放,自己也不能主动释放。
- 请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放;
- 循环等待条件:指在发生死锁时,必然存在一个线程请求资源的环形链,即线程集合 {T0,T1,T2,…Tn}中的 T0 正在等待一个 T1 占用的资源,T1 正在等待 T2 占用的资源,以此类推,Tn 正在等待己被 T0 占用的资源。
死锁的实现
场景设计:
- 创建 2 个线程,线程名分别为 threadA 和 threadB;
- 创建两个资源, 使用 new Object () 创建即可,分别命名为 resourceA 和 resourceB;
- threadA 持有 resourceA 并申请资源 resourceB;
- threadB 持有 resourceB 并申请资源 resourceA ;
- 为了确保发生死锁现象,请使用 sleep 方法创造该场景;
- 执行代码,看是否会发生死锁。
结果如下图所示,线程A获取A资源,线程B获取B资源。然后线程A由想要获取B资源,线程B又想获取A资源,他们谁也没有主动释放。
代码讲解:
- 从代码中来看,我们首先创建了两个资源 resourceA 和 resourceB;
- 然后创建了两条线程 threadA 和 threadB。threadA 首先获取了 resourceA ,获取的方式是代码 synchronized (resourceA) ,然后沉睡 1000 毫秒;
- 在 threadA 沉睡过程中, threadB 获取了 resourceB,然后使自己沉睡 1000 毫秒;
- 当两个线程都苏醒时,此时可以确定 threadA 获取了 resourceA,threadB 获取了 resourceB,这就达到了我们做的第一步,线程分别持有自己的资源;
- 那么第二步就是开始申请资源,threadA 申请资源 resourceB,threadB 申请资源 resourceA 无奈 resourceA 和 resourceB 都被各自线程持有,两个线程均无法申请成功,最终达成死锁状态。
package jvm.juc;
public class DeadLock {
private static Object resoureceA = new Object();
private static Object resoureceB = new Object();
public static void main(String[] args) {
Thread threadA = new Thread(()->{
System.out.println("线程A-准备获得A资源....");
synchronized (resoureceA) {
System.out.println("线程A-已经获得A资源");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程A-准备获得B资源");
synchronized (resoureceB) {
System.out.println("线程A-已经获得B资源");
}
}
});
threadA.setName("Thread-A");
Thread threadB = new Thread(()->{
System.out.println("线程B-准备获得B资源....");
synchronized (resoureceB) {
System.out.println("线程B-已经获得B资源");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程B-准备获得A资源");
synchronized (resoureceA) {
System.out.println("线程B-已经获得A资源");
}
}
});
threadA.setName("Thread-B");
threadA.start();
threadB.start();
}
}
如何避免线程死锁
要想避免死锁,只需要破坏掉至少一个构造死锁的必要条件即可,学过操作系统的读者应该都知道,目前只有请求并持有和环路等待条件是可以被破坏的。
我们,不让他们循环持有相互的资源就行。
package jvm.juc; public class DeadLock { private static Object resoureceA = new Object(); private static Object resoureceB = new Object(); public static void main(String[] args) { Thread threadA = new Thread(()->{ synchronized (resoureceA) { System.out.println("线程A-已经获得A资源"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (resoureceB) { System.out.println("线程A-已经获得B资源"); } } }); threadA.setName("Thread-A"); Thread threadB = new Thread(()->{ synchronized (resoureceA) { System.out.println("线程B-已经获得A资源"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (resoureceB) { System.out.println("线程B-已经获得B资源"); } } }); threadA.setName("Thread-B"); threadA.start(); threadB.start(); } }