一、什么是死锁?
在Java中,死锁是指两个或多个线程互相持有对方所需要的锁,并且在无法继续执行的情况下永久地等待对方释放锁。这种情况下,所有涉及的线程都无法继续执行,程序被卡住,无法正常终止。
死锁通常发生在多线程并发执行时,当线程之间相互竞争获取资源的时候。典型的死锁场景包括以下四个条件的同时满足:
- 互斥:至少有一个资源同时只能被一个线程占用;
- 占有并等待:一个线程已经占有一个资源,同时还等待另一个线程占有的资源;
- 不可剥夺:一个线程已经占有的资源不能被其他线程强制性剥夺;
- 环路等待:多个线程之间形成等待资源的环路。
当这四个条件同时满足时,就会导致死锁的发生。为了避免死锁,可以采取一些预防措施,例如合理地设计资源分配顺序、避免线程持有多个锁、使用定时锁等待等方法来减少死锁的风险。
二、产生死锁的三个典型场景
1、一个线程一把锁
如果一个线程对同一把锁,连续加了两次锁,就会产生死锁。当然如果锁是可重入锁则不会死锁,可重入锁,字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁,不会出现死锁的情况。(每次线程拿锁的时候,都会判断一下这个锁的已拥有者有没有这个线程,如果有则会放行,因此就不会出现一个线程连续加了两次锁,也就不会死锁)
解决方案:
无脑使用synchronized,因为它是可重入锁。
2、两个线程两把锁
public class demo2 {public static void main(String[] args) {Object locker1 = new Object();Object locker2 = new Object(); Thread t1 = new Thread(() -> { synchronized (locker1) {synchronized (locker2) {}}});t1.start();Thread t2 = new Thread(() -> {synchronized (locker2) {synchronized (locker1) {}}});t2.start();}
死锁原因:线程t1拿到锁1,线程t2拿到锁2。接着线程t1想要锁2,但此时锁2被线程t2占用着,t1无法获取,陷入阻塞等待,而t2想要锁1,但锁1被t1占用着。就这样t1和t2陷入僵局,谁也无法正常释放锁,从而形成了死锁。
解决办法
线程t1与线程t2的加锁顺序一样即可:
public class demo2 {public static void main(String[] args) {Object locker1 = new Object();Object locker2 = new Object(); Thread t1 = new Thread(() -> { synchronized (locker1) {synchronized (locker2) {}}});t1.start();Thread t2 = new Thread(() -> {synchronized (locker1) {synchronized (locker2) {}}});t2.start();}
3、N个线程M把锁
哲学家问题:
哲学家问题是计算机科学领域中的一个经典问题,它用来探讨在并发编程中可能出现的资源竞争和死锁问题。这个问题通常用于说明多线程和并发编程中的困难和挑战。
问题的设定是这样的:有一圈圆桌上有一些哲学家,每个哲学家面前放有一个盘子,圆桌上有一些餐叉,每两个相邻的哲学家之间有一把餐叉。哲学家的生活有两种状态:思考和进餐。当哲学家思考时,他们不需要任何资源,但当他们饿了的时候,他们会试图拿起左右两边的餐叉,如果两把餐叉都可用,他们就可以进餐,进餐完毕后,他们会放下餐叉继续思考。
问题的关键在于如何协调哲学家之间的行为,以避免死锁(所有哲学家都拿起一只餐叉,等待另一只,导致无法继续进餐)和资源竞争(多个哲学家同时试图拿起同一把餐叉,导致冲突)。
解决这个问题的方法包括使用各种同步机制,如互斥锁、信号量等,以确保哲学家在进餐时能够安全地占用需要的资源,避免发生死锁和资源竞争。
哲学家问题在并发编程中是一个经典的例子,用来说明如何有效地管理共享资源,以防止出现一些常见的并发问题。
解决办法
这个问题有很多方法的,其中一个方法是我们规定其中一个人等其他人都吃完了才可以拿筷子。按照这样的思路,这个死锁问题就可以得到解决。