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

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

一、什么是死锁

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

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


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


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


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

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

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


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的锁都无法正常释放,也都无法完成各自的进程,陷入了一个循环等待的状态


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


相关文章
关于死锁的原因及解决方案
关于死锁的原因及解决方案
190 0
|
27天前
|
监控 安全 定位技术
《C++新特性:为多线程数据竞争检测与预防保驾护航》
多线程编程是提升软件性能的关键,但数据竞争问题却是一大挑战。C++新特性如增强的原子类型和完善的内存模型,为检测和预防数据竞争提供了有力支持。这些改进不仅提高了程序的可靠性,还提升了开发效率,使多线程编程更加安全高效。
66 19
|
25天前
|
数据处理 数据库 开发者
《深度解析:死锁的“前世今生”与防范之道》
在计算机编程中,死锁如同隐藏的“定时炸弹”,可能导致系统瘫痪。本文深入解析死锁的定义、产生原因及预防策略,帮助开发者有效应对这一难题。通过破坏互斥、请求与保持、不可剥夺及循环等待条件,可显著降低死锁风险,保障系统稳定运行。
54 9
|
2月前
|
监控 安全 算法
线程死循环确实是多线程编程中的一个常见问题,在编码阶段规避潜在风险
【10月更文挑战第12天】线程死循环确实是多线程编程中的一个常见问题,在编码阶段规避潜在风险
61 2
|
4月前
|
资源调度 算法
深入理解网络中的死锁和活锁现象
【8月更文挑战第24天】
159 0
|
7月前
|
算法 Java
Java多线程基础-13:一文阐明死锁的成因及解决方案
死锁是指多个线程相互等待对方释放资源而造成的一种僵局,导致程序无法正常结束。发生死锁需满足四个条件:互斥、请求与保持、不可抢占和循环等待。避免死锁的方法包括设定加锁顺序、使用银行家算法、设置超时机制、检测与恢复死锁以及减少共享资源。面试中可能会问及死锁的概念、避免策略以及实际经验。
118 1
|
7月前
|
监控 IDE 测试技术
预防和处理线程死循环的关键步骤
【5月更文挑战第24天】预防和处理线程死循环的关键步骤包括理解死循环成因(逻辑错误、竞争条件、资源泄漏)、编码阶段采取预防措施(明确退出条件、避免无限递归、正确使用锁、资源管理、健壮的错误处理)、调试定位(断点、日志、线程分析工具、性能分析)、解决问题(修改代码、临时解决方案、逐步排查)以及测试验证(充分测试、专用测试用例)。遵循这些步骤可有效管理线程死循环风险。
130 1
|
6月前
|
算法 Java 开发者
深入理解死锁的原因、表现形式以及解决方法,对于提高Java并发编程的效率和安全性具有重要意义
【6月更文挑战第10天】本文探讨了Java并发编程中的死锁问题,包括死锁的基本概念、产生原因和解决策略。死锁是因线程间争夺资源导致的互相等待现象,常由互斥、请求与保持、非剥夺和循环等待条件引起。常见死锁场景包括资源请求顺序不一致、循环等待等。解决死锁的方法包括避免嵌套锁、设置锁获取超时、规定锁顺序、检测与恢复死锁,以及使用高级并发工具。理解并防止死锁有助于提升Java并发编程的效率和系统稳定性。
410 0
|
7月前
|
监控 安全
线程死循环是多线程应用程序开发过程中一个难以忽视的问题,它源于线程在执行过程中因逻辑错误或不可预见的竞争状态而陷入永久运行的状态,严重影响系统的稳定性和资源利用率。那么,如何精准定位并妥善处理线程死循环现象,并在编码阶段就规避潜在风险呢?谈谈你的看法~
避免线程死循环的关键策略包括使用同步机制(如锁和信号量)、减少共享可变状态、设置超时、利用监控工具、定期代码审查和测试、异常处理及设计简洁线程逻辑。通过这些方法,可降低竞态条件、死锁风险,提升程序稳定性和可靠性。
104 0

热门文章

最新文章