一、什么是死锁
所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。
比如我和我朋友在吃饺子,我朋友吃饺子蘸醋,我吃饺子蘸辣椒。但我觉得吃饺子光蘸辣椒不过瘾,即蘸辣椒又蘸醋才过瘾。于是我对我朋友说:兄弟,你先把你面前的醋给我好吗?等我吃完了饺子,就把醋和辣椒都给你!!!但我这个时候也对我说:我也想即蘸醋又蘸辣椒,要不你先把你的东西给我,我吃完了饺子,再把醋和辣椒都给你!!!
于是我们就产生了争执,谁也不肯把对方想要的给对方,同时我们又都很执拗,如果不能做到同时即蘸醋又蘸辣椒,就不吃饺子。结果我们谁也吃不完饺子,于是也无法把对方所需要的醋或者辣椒给对方。
其中,我和朋友就相当于是两个进程,醋和辣椒就是两把锁。我和朋友都想同时即蘸醋又蘸辣椒(获取到对方的锁)然后再结束各自的进程,释放锁。但谁都不肯先释放锁,都等着对方释放锁,结果就是谁都无法正常的释放锁,都陷入了阻塞等待中,这也被称为死锁。
二、产生死锁的三个典型场景
🌰案例一(一个线程一把锁)
如果一个线程对同一把锁,连续加了两次锁,并且该锁还是不可重入锁的时候,就会产生死锁。
对可重入锁和不可重入锁的补充
如果同一个线程在重复获取同一把锁的过程中,形成了死锁。这把锁又被称为不可重入锁。而可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁,不会出现死锁的情况。synchronized 是可重入锁
🌰案例二(两个线程两把锁)
package Thread2; import java.util.concurrent.*; import java.util.concurrent.locks.ReentrantLock; public class demo22 { private static Object locker1 = new Object(); // 相当于醋 private static Object locker2 = new Object(); // 相当于辣椒 public static void main(String[] args) { Thread t1 = new Thread(() -> { // t1线程相当于是我朋友,再有醋locker1的情况下,还想获取到我的辣椒locker2 synchronized (locker1) { System.out.println("我目前有醋,但我还想蘸辣椒"); try { Thread.sleep(1000); } catch (InterruptedException e) { // 正在和我交谈,想要获取辣椒 e.printStackTrace(); } synchronized (locker2) { System.out.println("获取辣椒成功!等我吃完饺子就把醋和辣椒都给对方!(释放锁)"); } } }); t1.start(); Thread t2 = new Thread(() -> { // t2线程相当于是我,再有辣椒locker2的情况下,还想获取到我朋友的醋locker1 synchronized (locker2) { System.out.println("我目前有辣椒,但我还想蘸醋"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (locker1) { System.out.println("获取醋成功!等我吃完饺子就把醋和辣椒都给对方!(释放锁)"); } } }); t2.start(); } }
死锁原因分析
// 死锁原因分析,线程t1给对象locker1加了锁,线程t2给对象locker2加了锁;
// 接着线程t1想要获取对象locker2的锁,但此时locker2被线程t2占用着,t1无法获取,陷入阻塞等待(也无法释放自己占用的对象locker1的锁)
// 几乎在同一时间,t2想要获取对象locker1的锁,但此时线程t1陷入阻塞,他所占用的locker1的锁无法正常释放。t2获取不到locker1的锁,t2无法正常工作,也无法正常释放自己占用的locker2的锁
// 就这样t1和t2陷入僵局,谁也无法正常释放锁,形成了死锁
解决办法
给我们的锁编号,按顺序来获取锁(规定都先蘸醋、接着蘸辣椒)
package Thread2; import java.util.concurrent.*; import java.util.concurrent.locks.ReentrantLock; public class demo22 { private static Object locker1 = new Object(); // 相当于醋 private static Object locker2 = new Object(); // 相当于辣椒 public static void main(String[] args) { Thread t1 = new Thread(() -> { // t1线程相当于是我朋友,一开始都有醋 synchronized (locker1) { System.out.println("我朋友说:我目前有醋,但我还想蘸辣椒"); try { Thread.sleep(1000); } catch (InterruptedException e) { // 正在和我交谈,想要获取辣椒 e.printStackTrace(); } synchronized (locker2) { System.out.println("我朋友说:获取辣椒成功!等我吃完饺子就把醋和辣椒都给对方!(释放锁)"); } } }); // 死锁的解决办法,多个线程在获取多个锁的时候,我们可以给这些锁编号。每个线程都按照锁的编号,从小到大的获取锁 // 一开始,我和我朋友要获取到的都是对象locker1的锁,产生竞争,竞争成功的获取到locker1的锁,失败的阻塞等待locker1锁的释放 // 竞争成功的接着又获取对象locker2的锁,此时因为另一个线程还在阻塞,没人和他竞争,直接获取locker2的锁,然后该线程结束,locker2锁、locker1锁按顺序释放 // 之前那个竞争失败的线程重写获取到locker1锁,接着又成功获取到locker2锁,最后线程结束,释放锁 t1.start(); Thread t2 = new Thread(() -> { // t2线程相当于是我,一开始都有醋 synchronized (locker1) { // 先获取编号为1的锁locker System.out.println("我说:目前有醋,但我还想蘸辣椒"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (locker2) { System.out.println("我说:获取辣椒成功!等我吃完饺子就把醋和辣椒都给对方!(释放锁)"); } } }); t2.start(); } }
🌰案例三(N个线程M把锁)
哲学家吃面条
5位哲学家围着一张桌子,桌子上有几碗面条。这5位哲学家的左右手两边各有一根筷子(注意是一根,不是一双,两根筷子才是一双,才能拿来吃面,一根筷子无法吃面)
5位哲学家相当于是5个线程,这些线程只有分别拿到左右手旁的两根筷子(各自要求的两把锁),才能完成进程,并释放自己所占用的锁。
然后呢,在某一时刻,哲学家都想吃面条:他们同时拿起了自己右手边的那根筷子。5位哲学家、5根筷子,他们每个人都只拿了一根筷子(获取到了一个锁) 。于是他们每个人都完成不了各自的进程,也无法释放他们所占用的锁(筷子),都吃不到面条。
这又是一个死锁问题
解决办法
那么怎么解决呢?和上面死锁的解决方案相同——我们要分析为什么会出现死锁,就是因为线程对锁的互相等待,线程一要获取的锁被线程二占用着,但同时线程二要获取的锁又被线程一占用着,于是他们两个都无法获取到完整的锁,无法完成各自的进程,并释放锁。都处于一个循环等待的过程。
要解决死锁问题,重点就是解决循环等待问题。如果每个线程都按一定的顺序来获取对应的锁,比如在上面的栗子中,我们给5根筷子(5把锁)按从1到5的顺序进行编号,哲学家只能拿到到左右两边锁编号最小的那把锁。(已经拿到的锁不用进行编号的比较)
按照这样的思路,这个死锁问题就可以得到解决
三、形成死锁的四个条件
从上述几个死锁的案例中我们也可以得到形成死锁的四个条件
互斥性:当多个线程对同一把锁,有竞争。在某一时刻,最终只有一个线程可以拥有这把锁
不可抢夺性:当一个线程已经获取到了锁A,其他线程要想获取锁A,这个时候只能等该线程把A释放了之后再获取,不能中途抢夺别的线程的锁。
请求和保持性:当一个线程获取到了锁A,除非该线程自己释放锁A,否则该线程就一直保持占有锁A
循环等待性:在死锁中往往会出现,线程A等着线程B释放锁,同时线程B又在等着线程A来释放他所占有的锁,结果A、B的锁都无法正常释放,也都无法完成各自的进程,陷入了一个循环等待的状态
只要这四个条件当中有一个条件被破坏,死锁问题就可以得到解决。其中循环等待性这个条件最容易被破坏——我们上面的对锁进行编号,来解决死锁问题。利用的就是对循环等待性的破坏。