前言
死锁(Deadlock)是指两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象,如果不提前预防或外界干扰,这些线程将无法执行下去。因此,本篇博文讲解造成死锁的原因以及解决方案。
目录
1. 造成死锁的原因
2. 哲学家就餐问题
2.1 造成死锁的原因
2.2 解决死锁问题
1. 造成死锁的原因
死锁是多个线程在相互等待对方所占有的锁才能继续执行下去,造成线程都被阻塞都无法执行下去,也释放不了线程自己的锁资源,造成僵局。
举例说明:
我和朋友去拉面馆吃面,我先拿起了辣椒他拿起了醋。此时他想要辣椒、我想要醋,但我俩谁也不让谁。
我对他说:你先把辣椒给我,我用完了就把醋给你。他对我说:你先把醋给我,我用完了就把辣椒给你。
我和他也不让谁,最后导致死锁。在上述例子中,辣椒和醋就是两个锁,我和他就是两个线程。关于死锁会引申的一个哲学家就餐问题。
2. 哲学家就餐问题
哲学家就餐问题是一个经典的并发编程问题,它描述的是五个哲学家围坐在一张圆形餐桌旁,每个哲学家面前有一碗饭和一只筷子,碗和筷子都是共享资源。
哲学家需要用一双筷子来吃饭,但是只有相邻的两个哲学家之间有一只筷子,因此他们必须在餐桌上竞争筷子,才能拿到一双筷子吃饭。
这个问题会引发死锁(Deadlock)和饥饿(Starvation)等并发编程中的经典问题。
如果所有哲学家同时都想获取餐具,但每个哲学家只能等待旁边的哲学家放下筷子才能获取餐具,这可能会导致死锁。
如果所有哲学家只有在其他人都吃到饭才能拿到自己的餐具,那么有些哲学家可能会因为等待时间太长而饥饿。
代码案例:
有两个哲学家在进行就餐,哲学家1获取到筷子1后继续尝试获取筷子2,哲学家2获取到筷子2后继续尝试获取筷子1,造成了死锁状态。
public class DeadlockDemo {
public static void main(String[] args) {
Object obj1 = new Object();//筷子1
Object obj2 = new Object();//筷子2
// 线程1获取obj1,然后等待obj2
Thread thread1 = new Thread(() -> {
synchronized (obj1) {
System.out.println(Thread.currentThread().getName() + " 获取 obj1锁");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj2) {
System.out.println(Thread.currentThread().getName() + " 获取 obj2锁");
}
}
}, "哲学家 1");
// 线程2获取obj2,然后等待obj1
Thread thread2 = new Thread(() -> {
synchronized (obj2) {
System.out.println(Thread.currentThread().getName() + " 获取 obj2锁");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj1) {
System.out.println(Thread.currentThread().getName() + " 获取 obj1锁");
}
}
}, "哲学家 2");
t1.start();
t2.start();
}
}
运行后打印:
上述代码,中 thread1 是哲学家1,thread2 是哲学家2,obj1 锁和 obj2 锁分别代表两只筷子。哲学家1 占用了 obj1 锁,哲学家2 占用了 obj2 锁。
此时 哲学家1 在拥有 obj1 时获取 obj2 是获取不到的,哲学家2 在拥有 obj2 时获取 obj1 也是获取不到的。
这样就会造成死锁。也就是本篇文章开头所说的,一个线程处于永久获取到锁的状态,其他线程无限阻塞等待,这就造成了死锁。
2.1 造成死锁的原因
- 互斥:一个资源只能被一个进程独占,即该资源只能被一个进程占有或使用。
- 请求和保持:进程已经保持了至少一个资源,并还在尝试获取其他进程保持的资源。
- 不可抢占:某个资源只能由占有它的线程释放,不能被其他线程强制占有或抢占。
- 循环等待:假设一个队列有{t1,t2,t3}这三个线程,t1 等待 t2 所占资源,t2 等待 t3 所占资源,t3 等待 t1 所占资源,这样就会造成循环等待。
2.2 解决死锁问题
解决死锁问题,最好的方式就是预防死锁。我们把锁按照顺序进行编号,并且约定每个线程在获取多把锁的时候必须先获取编号小的,后获取编号大的。这样就不会形成死锁了。
因为,最先获取编号小锁的线程会优先完成相应任务。轮到其他线程再获取小编号锁时,锁就处于空闲状态了。
代码案例:
public class DeadlockDemo {
public static void main(String[] args) {
Object obj1 = new Object();//筷子1
Object obj2 = new Object();//筷子2
Thread t1 = new Thread(() -> {
synchronized (obj1) {
System.out.println(Thread.currentThread().getName() + " 获取 obj1锁");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj2) {
System.out.println(Thread.currentThread().getName() + " 获取 obj2锁");
}
}
}, "哲学家 1");
Thread t2 = new Thread(() -> {
synchronized (obj1) {
System.out.println(Thread.currentThread().getName() + " 获取 obj2锁");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj2) {
System.out.println(Thread.currentThread().getName() + " 获取 obj1锁");
}
}
}, "哲学家 2");
t1.start();
t2.start();
}
}
运行后打印:
在上述代码中,我们创建了两个锁对象 obj1 和 obj2 分别代表两只筷子,并创建两个线程 thread1 和 thread 2 分别代表两个哲学家。
当 哲学家1 首先获取到 obj1 后,由于 obj2 并未线程占用,则立马获取 obj2 ,获得了两双筷子,就餐后释放 obj1 和 obj2。
这时 哲学家2 尝试获取 obj2 和 obj1 时,发现两双筷子都没人使用,直接开始就餐。
什么是死锁?
当多个线程并发后,线程在占有锁资源的情况下还尝试获取别的线程所占有的锁资源。这样就会操作线程永远获取不到其他锁资源,也不能释放本身锁资源。从而形成的一种僵局,这就是死锁。
造成死锁的原因是什么?
造成死锁的原因有,互斥、请求和保持、不可抢占、循环等待。(文章中有讲解)
死锁的解决方案
我们可以对多个锁按照顺序进行编号,并约定每个线程在使用多把锁时优先使用最小编号的锁,这样就能保证其他线程再获取小编号锁时,锁资源已得到释放。
本篇博文到这里就结束了,感谢点赞、评论、收藏、关注~