Java多线程中的死锁详解
作者:一只爱打拳的程序猿
前言
死锁(Deadlock)是指两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象
如果不提前预防或外界干扰,这些线程将无法执行下去。
因此,本篇博文讲解造成死锁的原因以及解决方案。
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 时,发现两双筷子都没人使用,直接开始就餐。
总结
什么是死锁?
当多个线程并发后,线程在占有锁资源的情况下还尝试获取别的线程所占有的锁资源。
这样就会操作线程永远获取不到其他锁资源,也不能释放本身锁资源。
从而形成的一种僵局,这就是死锁。
造成死锁的原因是什么?
造成死锁的原因有,互斥、请求和保持、不可抢占、循环等待。(文章中有讲解)
死锁的解决方案
我们可以对多个锁按照顺序进行编号,并约定每个线程在使用多把锁时优先使用最小编号的锁,这样就能保证其他线程再获取小编号锁时,锁资源已得到释放。
到此这篇关于Java多线程中的死锁详解的文章就介绍到这了,更多相关Java多线程死锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!