• 死锁问题【javaEE初阶】


    什么是死锁?
            所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。 因此我们举个例子来描述,如果此时有一个线程A,按照先锁a再获得锁b的的顺序获得锁,而在此同时又有另外一个线程B,按照先锁b再锁a的顺序获得锁。 

    一、"死锁"出现的典型场景

    (一)一个线程一把锁连续加锁两次,如果锁是不可重入锁,就会死锁

    一个线程一把锁,这个就是之前所介绍的 可重入锁 

    线程A 针对锁1 连续加锁两次,就构成了死锁 !

    第一次加锁 加锁成功,第二次加锁 就需要第一次的锁释放,于是就阻塞等待;

    但是,第一次加锁释放,就得依赖第二次加锁成功;

    于是,"死锁" 就出现了!!!

    (二)两个线程两把锁,相互获取对方锁 

    此时,不管是不是可重入锁,都会造成 "死锁" 问题 !

    可以用一个例子来说明情况:

    假设 甲同学 和 乙同学 在一起

    甲同学 吃饺子喜欢蘸酱油,乙同学 吃饺子喜欢蘸醋,由于在一起了,生活习性都受到了彼此的影响,两位同学吃饺子的时候 喜欢都蘸上一点了

    有一天,吃饺子的时候,甲同学拿起了酱油,乙同学拿起了醋 ~

    甲同学说:"你把醋给我,我用完了都给你";乙同学说:"你把酱油给我,我用完了就把醋给你" ~

    此时,两者相持不下,这就造成了 "死锁" 问题 

    此时,甲同学 和 乙同学 就可以看成是两个线程,酱油和醋 就可以看成是两把锁 

    线程1 获取到锁A,线程2 获取到锁B;

    线程1 尝试获取锁B(需要线程2 释放锁B) ,线程2 尝试获取到锁A(需要线程1 释放锁A);

    在这种情况下,逻辑上就构成了循环,就构成了 "死锁" ~ 

    1. import java.util.concurrent.TimeUnit;
    2. public class Main {
    3. public static void main(String[] args) {
    4. Object lock1 = new Object();
    5. Object lock2 = new Object();
    6. Thread t1 = new Thread(() -> {
    7. synchronized (lock1) {
    8. System.out.println( "t1:已经获取到锁1");
    9. try {
    10. Thread.sleep(1);
    11. } catch (InterruptedException e) {
    12. e.printStackTrace();
    13. }
    14. synchronized (lock2) {
    15. System.out.println( "t1:获取到锁2");
    16. }
    17. }
    18. }, "t1");
    19. t1.start();
    20. Thread t2 = new Thread(() -> {
    21. synchronized (lock2) {
    22. System.out.println( "t2:获取到锁2");
    23. try {
    24. Thread.sleep(1);
    25. } catch (InterruptedException e) {
    26. e.printStackTrace();
    27. }
    28. synchronized (lock1) {
    29. System.out.println( "t2:获取到锁1");
    30. }
    31. }
    32. }, "t2");
    33. t2.start();
    34. }
    35. }

     一直等待,死锁了

    (三)多个线程多个锁,"哲学家就餐问题

    此时,这种情况和 第二种情况 类似,只是更复杂一点而已 

    在多个线程多把锁的情况下,"死锁"问题 就是一个概率性的问题,但是也绝对不能忽视 ! 

    在谈到 "多个线程多把锁" 的时候,就会引出一个很经典的问题 —— "哲学家就餐问题" ! 

    故事背景:

    有 5 个哲学家,相当于有 5 个线程,他们只会做两件事情:

            思考人生(相当于是 线程休眠)
            吃面条(相当于是 在CPU上运行)
    由于多线程的调度是无序的,所以说 这几个哲学家 什么时候去思考人生,什么时候去吃面条,我们是不确定的  

    但是,此时 在餐桌上 一共只有 5 根筷子,并且 这5 根筷子 分别在 两两哲学家 之间 

    并且,他们之间都不相互嫌弃,吃面条的时候要拿起 左右手两双筷子(这就导致相邻的哲学家需要等待) ~

    此处的筷子就视为 两把"锁",只有这两把锁都获取到了,才可以吃面条 ~ 

    出现 "死锁" 问题的情况:

    假设 在同一时刻,所有的哲学家都想吃面条 ~

    他们同时伸出左手,拿起左边的筷子;然后又同时伸出右手,尝试去拿右边的筷子;此时,右手的筷子都拿不起来,因此都无法吃面 

    由于哲学家都非常固执,导致即使吃不到面条 也不会放下左手的筷子,这样的情况就会一直持续下去  ,这就构成了死锁  


     二、产生 "死锁" 的必要条件

    鉴于 "死锁"问题,程序员大佬们 总结了 4 个 "死锁"产生的必要条件:

    1. 互斥使用(线程1 拿到锁A,其他线程就无法获取到 A)
    2. 不可抢占(线程1 拿到锁A,其他线程只能阻塞等待,等到线程1 主动释放锁,而不是强行把锁抢走)
    3. 请求和保持(当线程1 拿到锁A 之后,就会一直持有这个获取到锁的状态,直到说主动释放锁) 
    4. 循环等待(线程1 等待线程2,线程2 又尝试等待线程1)

    三、解决 "死锁" 问题的办法

    1. 资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)
    2. 只要有一个资源得不到分配,也不给这个进程分配其他的资源:(破坏请保持条件)
    3. 可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
    4. 资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件) 

    根据 产生 "死锁" 的必要条件,我们可以知道,前三个必要条件 都是在描述锁的基本特点,在实际情况下 我们并不好直接去破坏 

    但是,第四个必要条件 却是和代码编写密切相关  

    如果我们能够在编码上做出一些注意和约定,就可以打破 "循环等待",避免死锁 !!!

    1、以确定的顺序获得锁

             针对两个特定的锁,开发者可以尝试按照锁对象的hashCode值大小的顺序,分别获得两个锁,这样锁总是会以特定的顺序获得锁,那么死锁也不会发生。问题变得更加复杂一些,如果此时有多个线程,都在竞争不同的锁,简单按照锁对象的hashCode进行排序(单纯按照hashCode顺序排序会出现“环路等待”),可能就无法满足要求了,这个时候开发者可以使用银行家算法,所有的锁都按照特定的顺序获取,同样可以防止死锁的发生。

    针对多把锁,进行编号:1、2、3、4、......

    并且约定在获取多把锁的时候,要明确获取锁的顺序是 从小到大(或者 从大到小) 的顺序 

    如(此处以从小到大为例):

    线程要拿到 1、2 这两把锁,就先获取 1,再获取 2;

    线程要拿到 2、4 这两把锁,就先获取 2,再获取 4 ~

    只要所有的线程都遵循这个顺序,就不会出现 "循环等待",就不会出现死锁 !!!  

    我们可以把这个解决办法 带入到上面的 "哲学家就餐问题" 看看 ~

    约定:获取所得顺序是:从小到大 ~

     

     之后,最左边的哲学家 就可以得到两把锁了,于是就可以吃到面条了 ~

    等到 吃完面条之后,会释放 4、5两把筷子;然后 最上面的哲学家 也可以吃到面条了,......,就这样的话,顺时针旋转,依次 5 位哲学家都可以吃上面条了 

    2、超时放弃

            当使用synchronized关键词提供的内置锁时,只要线程没有获得锁,那么就会永远等待下去,然而Lock接口提供了boolean tryLock(long time, TimeUnit unit) throws InterruptedException方法,该方法可以按照固定时长等待锁,因此线程可以在获取锁超时以后,主动释放之前已经获得的所有的锁。通过这种方式,也可以很有效地避免死锁。 

    避免死锁:

    • 预防死锁的几种策略,会严重地损害系统性能。因此在避免死锁时,要施加较弱的限制,从而获得 较满意的系统性能。由于在避免死锁的策略中,允许进程动态地申请资源。因而,系统在进行资源分配之前预先计算资源分配的安全性。若此次分配不会导致系统进入不安全的状态,则将资源分配给进程;否则,进程等待。其中最具有代表性的避免死锁算法是银行家算法。
    • 银行家算法:首先需要定义状态和安全状态的概念。系统的状态是当前给进程分配的资源情况。因此,状态包含两个向量Resource(系统中每种资源的总量)和Available(未分配给进程的每种资源的总量)及两个矩阵Claim(表示进程对资源的需求)和Allocation(表示当前分配给进程的资源)。安全状态是指至少有一个资源分配序列不会导致死锁。当进程请求一组资源时,假设同意该请求,从而改变了系统的状态,然后确定其结果是否还处于安全状态。如果是,同意这个请求;如果不是,阻塞该进程知道同意该请求后系统状态仍然是安全的。

    检测死锁

    1. 首先为每个进程和每个资源指定一个唯一的号码;
    2. 然后建立资源分配表和进程等待表。

    解除死锁:

    当发现有进程死锁后,便应立即把它从死锁状态中解脱出来,常采用的方法有:

    1. 剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态;
    2. 撤消进程:可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等。 

    死锁检测

    1、Jstack命令

            jstack是java虚拟机自带的一种堆栈跟踪工具。jstack用于打印出给定的java进程ID或core file或远程调试服务的Java堆栈信息。 Jstack工具可以用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。

    2、JConsole工具

            Jconsole是JDK自带的监控工具,在JDK/bin目录下可以找到。它用于连接正在运行的本地或者远程的JVM,对运行在Java应用程序的资源消耗和性能进行监控,并画出大量的图表,提供强大的可视化界面。而且本身占用的服务器内存很小,甚至可以说几乎不消耗。 

  • 相关阅读:
    esp32 Micropython 使用多线程驱动EC11编码器
    基于Java+vue前后端分离高校疫情管理系统设计实现(源码+lw+部署文档+讲解等)
    GBase 8d的特性-高性能
    【bug】uniapp的image组件渲染gif图,只有第一次点击的时候有动效,需要每次点击都有gif效果,已解决
    Ros Cmakelist 编译配置
    mybatis批量更新
    Html5+Css3第一讲:html5+Css3基础(1)
    推荐几个实用的在线小工具~
    Python环境的搭建+第一个Python程序的运行
    Vulnhub靶机:PRESIDENTIAL_ 1
  • 原文地址:https://blog.csdn.net/m0_65601072/article/details/128140106