死锁的成因和对应的解决方案

简介: 死锁的成因和对应的解决方案

一、什么是死锁

所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。

比如我和我朋友在吃饺子,我朋友吃饺子蘸醋,我吃饺子蘸辣椒。但我觉得吃饺子光蘸辣椒不过瘾,即蘸辣椒又蘸醋才过瘾。于是我对我朋友说:兄弟,你先把你面前的醋给我好吗?等我吃完了饺子,就把醋和辣椒都给你!!!但我这个时候也对我说:我也想即蘸醋又蘸辣椒,要不你先把你的东西给我,我吃完了饺子,再把醋和辣椒都给你!!!


于是我们就产生了争执,谁也不肯把对方想要的给对方,同时我们又都很执拗,如果不能做到同时即蘸醋又蘸辣椒,就不吃饺子。结果我们谁也吃不完饺子,于是也无法把对方所需要的醋或者辣椒给对方。


其中,我和朋友就相当于是两个进程,醋和辣椒就是两把锁。我和朋友都想同时即蘸醋又蘸辣椒(获取到对方的锁)然后再结束各自的进程,释放锁。但谁都不肯先释放锁,都等着对方释放锁,结果就是谁都无法正常的释放锁,都陷入了阻塞等待中,这也被称为死锁。


二、产生死锁的三个典型场景

🌰案例一(一个线程一把锁)

如果一个线程对同一把锁,连续加了两次锁,并且该锁还是不可重入锁的时候,就会产生死锁。


6b0775122cf4478b8e5539423c0ddae4.png

对可重入锁和不可重入锁的补充

如果同一个线程在重复获取同一把锁的过程中,形成了死锁。这把锁又被称为不可重入锁。而可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁,不会出现死锁的情况。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陷入僵局,谁也无法正常释放锁,形成了死锁


c9c2cc6c1ff04e4697abcede48bb5653.png

0d432137142541e8a841a1bbe5c934c8.png

解决办法

给我们的锁编号,按顺序来获取锁(规定都先蘸醋、接着蘸辣椒)

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();
    }
}


1e2fac189ac7467facb63b1628d015f8.png

🌰案例三(N个线程M把锁)

哲学家吃面条


5位哲学家围着一张桌子,桌子上有几碗面条。这5位哲学家的左右手两边各有一根筷子(注意是一根,不是一双,两根筷子才是一双,才能拿来吃面,一根筷子无法吃面)


5位哲学家相当于是5个线程,这些线程只有分别拿到左右手旁的两根筷子(各自要求的两把锁),才能完成进程,并释放自己所占用的锁。  

fd237da02a4e4df8ba10a03465fbb556.png

然后呢,在某一时刻,哲学家都想吃面条:他们同时拿起了自己右手边的那根筷子。5位哲学家、5根筷子,他们每个人都只拿了一根筷子(获取到了一个锁) 。于是他们每个人都完成不了各自的进程,也无法释放他们所占用的锁(筷子),都吃不到面条。

这又是一个死锁问题

解决办法

那么怎么解决呢?和上面死锁的解决方案相同——我们要分析为什么会出现死锁,就是因为线程对锁的互相等待,线程一要获取的锁被线程二占用着,但同时线程二要获取的锁又被线程一占用着,于是他们两个都无法获取到完整的锁,无法完成各自的进程,并释放锁。都处于一个循环等待的过程。

要解决死锁问题,重点就是解决循环等待问题。如果每个线程都按一定的顺序来获取对应的锁,比如在上面的栗子中,我们给5根筷子(5把锁)按从1到5的顺序进行编号,哲学家只能拿到到左右两边锁编号最小的那把锁。(已经拿到的锁不用进行编号的比较)


57ccb37d04ff40519e7bda7992318d4b.png

按照这样的思路,这个死锁问题就可以得到解决

三、形成死锁的四个条件

从上述几个死锁的案例中我们也可以得到形成死锁的四个条件

互斥性:当多个线程对同一把锁,有竞争。在某一时刻,最终只有一个线程可以拥有这把锁

不可抢夺性:当一个线程已经获取到了锁A,其他线程要想获取锁A,这个时候只能等该线程把A释放了之后再获取,不能中途抢夺别的线程的锁。

请求和保持性:当一个线程获取到了锁A,除非该线程自己释放锁A,否则该线程就一直保持占有锁A

循环等待性:在死锁中往往会出现,线程A等着线程B释放锁,同时线程B又在等着线程A来释放他所占有的锁,结果A、B的锁都无法正常释放,也都无法完成各自的进程,陷入了一个循环等待的状态


只要这四个条件当中有一个条件被破坏,死锁问题就可以得到解决。其中循环等待性这个条件最容易被破坏——我们上面的对锁进行编号,来解决死锁问题。利用的就是对循环等待性的破坏。


相关文章
|
9月前
关于死锁的原因及解决方案
关于死锁的原因及解决方案
94 0
|
11天前
|
监控 安全
线程死循环是多线程应用程序开发过程中一个难以忽视的问题,它源于线程在执行过程中因逻辑错误或不可预见的竞争状态而陷入永久运行的状态,严重影响系统的稳定性和资源利用率。那么,如何精准定位并妥善处理线程死循环现象,并在编码阶段就规避潜在风险呢?谈谈你的看法~
避免线程死循环的关键策略包括使用同步机制(如锁和信号量)、减少共享可变状态、设置超时、利用监控工具、定期代码审查和测试、异常处理及设计简洁线程逻辑。通过这些方法,可降低竞态条件、死锁风险,提升程序稳定性和可靠性。
14 0
|
4月前
|
测试技术
如何避免测试同化现象?
如何避免测试同化现象?
|
4月前
|
算法
出现线程死锁缺陷一般有那些原因?该怎么解决?
出现线程死锁缺陷一般有那些原因?该怎么解决?
30 1
多线程之探讨死锁的成因和解决方案
多线程之探讨死锁的成因和解决方案
多线程之探讨死锁的成因和解决方案
|
5月前
|
Java 调度
没了解死锁怎么能行?进来看看,一文带你拿下死锁产生的原因、死锁的解决方案。
没了解死锁怎么能行?进来看看,一文带你拿下死锁产生的原因、死锁的解决方案。
24 0
|
10月前
|
安全
导致并发程序出现问题的根本原因是什么?
导致并发程序出现问题的根本原因是多线程之间的竞争条件和共享资源的访问冲突。多线程环境下,多个线程同时访问和修改共享资源时,可能会导致数据不一致、竞态条件、死锁等问题。
149 0
|
9月前
|
Java
死锁不可怕
死锁不可怕
55 0
|
SQL 监控 关系型数据库
死锁分析延续
可知上一篇【死锁分析】,又重新表达了一些图片,图画更了
126 0
死锁分析延续
|
算法 安全
解决死锁——银行家算法透析
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。 避免死锁算法中最有代表性的算法是Dijkstra E.W 于1968年提出的银行家算法:
解决死锁——银行家算法透析